Vue3+uni-app全栈开发:一番赏手办商城小程序实战指南

最近在开发一番赏手办商城小程序时,发现很多开发者对Vue+uni-app的全栈开发流程不够熟悉,特别是前后端分离架构下的接口对接和后台管理系统设计。本文将完整分享一套基于Vue3+uni-app的一番赏商城开发方案,包含小程序前端和后台管理系统,附带完整的接口文档说明。

1. 项目概述与技术选型

1.1 一番赏商城业务特点

一番赏是源自日本的抽奖式手办销售模式,具有以下业务特点:

  • 多奖池设计:每个奖池包含A赏、B赏、C赏等不同等级奖品
  • 概率抽奖:用户支付固定金额参与抽奖
  • 库存管理:每个奖品的库存需要精确控制
  • 订单系统:支持抽奖记录、发货状态跟踪

1.2 技术栈选择理由

前端技术栈:Vue3 + uni-app + Vite

  • Vue3 Composition API更适合复杂状态管理
  • uni-app支持多端发布(微信小程序、H5、App)
  • Vite提供更快的开发体验和构建速度

后端技术栈:Node.js + Express + MySQL

  • 轻量级且性能优秀
  • 与前端技术栈同属JavaScript生态
  • 易于部署和维护

后台管理系统:Vue3 + Element Plus

  • 组件丰富,开发效率高
  • 与小程序前端技术栈统一

2. 开发环境搭建

2.1 环境要求与工具准备

确保本地环境满足以下要求:

  • Node.js 16.0 或更高版本
  • HBuilder X 或 VS Code
  • MySQL 5.7 或更高版本
  • 微信开发者工具

2.2 项目初始化

创建uni-app项目:

# 使用Vite创建uni-app项目 npm create uni-app@latest ichiban-mall cd ichiban-mall # 安装依赖 npm install # 安装必要的UI库 npm install @dcloudio/uni-ui npm install sass -D

创建后端项目结构:

# 创建后端目录 mkdir ichiban-server cd ichiban-server # 初始化package.json npm init -y # 安装后端依赖 npm install express mysql2 cors bcryptjs jsonwebtoken npm install nodemon -D

2.3 数据库设计

创建核心数据表结构:

-- 奖池表 CREATE TABLE prize_pools ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, description TEXT, price DECIMAL(10,2) NOT NULL, total_tickets INT DEFAULT 0, remaining_tickets INT DEFAULT 0, status ENUM('active', 'inactive') DEFAULT 'active', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 奖品表 CREATE TABLE prizes ( id INT PRIMARY KEY AUTO_INCREMENT, pool_id INT NOT NULL, level ENUM('A', 'B', 'C', 'D', 'E', 'F', 'G') NOT NULL, name VARCHAR(100) NOT NULL, image_url VARCHAR(255), total_stock INT NOT NULL, remaining_stock INT NOT NULL, probability DECIMAL(5,4) NOT NULL, FOREIGN KEY (pool_id) REFERENCES prize_pools(id) ); -- 用户表 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(100) UNIQUE, nickname VARCHAR(100), avatar_url VARCHAR(255), balance DECIMAL(10,2) DEFAULT 0.00, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 订单表 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(50) UNIQUE NOT NULL, user_id INT NOT NULL, pool_id INT NOT NULL, amount DECIMAL(10,2) NOT NULL, status ENUM('pending', 'paid', 'shipped', 'completed') DEFAULT 'pending', prize_id INT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (pool_id) REFERENCES prize_pools(id), FOREIGN KEY (prize_id) REFERENCES prizes(id) );

3. 前端小程序开发

3.1 项目目录结构规划

src/ ├── pages/ # 页面文件 │ ├── index/ # 首页 │ ├── pool-detail/ # 奖池详情 │ ├── lottery/ # 抽奖页面 │ ├── order/ # 订单页面 │ └── profile/ # 个人中心 ├── components/ # 公共组件 │ ├── prize-card/ # 奖品卡片 │ ├── lottery-wheel/ # 抽奖转盘 │ └── tab-bar/ # 自定义tabbar ├── stores/ # 状态管理 │ ├── user.js # 用户状态 │ └── lottery.js # 抽奖状态 ├── api/ # 接口封装 │ ├── request.js # 请求封装 │ ├── user.js # 用户接口 │ ├── pool.js # 奖池接口 │ └── order.js # 订单接口 ├── utils/ # 工具函数 │ ├── auth.js # 登录验证 │ ├── common.js # 通用函数 │ └── wxapi.js # 微信API封装 └── static/ # 静态资源

3.2 核心页面开发

首页实现(index.vue):

<template> <view class="container"> <!-- 轮播图 --> <swiper class="banner-swiper" indicator-dots autoplay circular> <swiper-item v-for="banner in banners" :key="banner.id"> <image :src="banner.image" mode="aspectFill" class="banner-image" /> </swiper-item> </swiper> <!-- 奖池列表 --> <view class="pool-section"> <view class="section-title">热门一番赏</view> <scroll-view class="pool-list" scroll-x> <view v-for="pool in prizePools" :key="pool.id" class="pool-item"> <prize-card :pool="pool" @click="goToPoolDetail(pool.id)" /> </view> </scroll-view> </view> <!-- 抽奖记录 --> <view class="record-section"> <view class="section-title">最新中赏</view> <view v-for="record in recentRecords" :key="record.id" class="record-item"> <image :src="record.user_avatar" class="avatar" /> <text class="username">{{ record.user_nickname }}</text> <text class="prize-info">抽中了{{ record.prize_name }}</text> </view> </view> </view> </template> <script setup> import { ref, onMounted } from 'vue' import { onLoad } from '@dcloudio/uni-app' import { getPrizePools, getRecentRecords } from '@/api/pool' import { getBanners } from '@/api/common' const banners = ref([]) const prizePools = ref([]) const recentRecords = ref([]) onLoad(async () => { await loadHomeData() }) const loadHomeData = async () => { try { const [bannerRes, poolRes, recordRes] = await Promise.all([ getBanners(), getPrizePools({ page: 1, limit: 10 }), getRecentRecords({ limit: 5 }) ]) banners.value = bannerRes.data prizePools.value = poolRes.data.list recentRecords.value = recordRes.data } catch (error) { uni.showToast({ title: '数据加载失败', icon: 'none' }) } } const goToPoolDetail = (poolId) => { uni.navigateTo({ url: `/pages/pool-detail/pool-detail?id=${poolId}` }) } </script> <style scoped> .container { padding: 20rpx; } .banner-swiper { height: 300rpx; border-radius: 16rpx; overflow: hidden; } .banner-image { width: 100%; height: 100%; } .section-title { font-size: 32rpx; font-weight: bold; margin: 30rpx 0 20rpx; } .pool-list { white-space: nowrap; } .pool-item { display: inline-block; width: 280rpx; margin-right: 20rpx; } .record-item { display: flex; align-items: center; padding: 20rpx; background: #f5f5f5; border-radius: 12rpx; margin-bottom: 16rpx; } .avatar { width: 60rpx; height: 60rpx; border-radius: 50%; margin-right: 20rpx; } .username { font-size: 28rpx; color: #666; margin-right: 10rpx; } .prize-info { font-size: 28rpx; color: #e74c3c; font-weight: bold; } </style>

抽奖转盘组件(lottery-wheel.vue):

<template> <view class="lottery-wheel"> <view class="wheel-container" :style="wheelStyle"> <view v-for="(prize, index) in prizes" :key="prize.id" class="wheel-item" :style="getItemStyle(index)" > <image v-if="prize.image" :src="prize.image" class="prize-image" /> <text class="prize-name">{{ prize.name }}</text> </view> </view> <view class="lottery-btn" @click="startLottery" :class="{ disabled: isLotting }"> {{ isLotting ? '抽奖中...' : '开始抽奖' }} </view> </view> </template> <script setup> import { ref, computed, defineProps, defineEmits } from 'vue' const props = defineProps({ prizes: { type: Array, default: () => [] }, duration: { type: Number, default: 4000 } }) const emit = defineEmits(['lottery-start', 'lottery-end']) const isLotting = ref(false) const currentAngle = ref(0) const wheelStyle = computed(() => ({ transform: `rotate(${currentAngle.value}deg)`, transition: isLotting.value ? `transform ${props.duration}ms cubic-bezier(0.2, 0.8, 0.3, 1)` : 'none' })) const getItemStyle = (index) => { const angle = 360 / props.prizes.length return { transform: `rotate(${index * angle}deg)`, transformOrigin: 'center center' } } const startLottery = async () => { if (isLotting.value) return isLotting.value = true emit('lottery-start') // 模拟抽奖结果(实际应调用接口) const resultIndex = Math.floor(Math.random() * props.prizes.length) const targetAngle = 360 - (resultIndex * (360 / props.prizes.length)) + 360 * 5 currentAngle.value = targetAngle setTimeout(() => { isLotting.value = false emit('lottery-end', props.prizes[resultIndex]) }, props.duration) } </script> <style scoped> .lottery-wheel { display: flex; flex-direction: column; align-items: center; } .wheel-container { width: 600rpx; height: 600rpx; border-radius: 50%; background: linear-gradient(45deg, #ff6b6b, #ffa726, #66bb6a, #42a5f5); position: relative; overflow: hidden; } .wheel-item { position: absolute; width: 50%; height: 50%; left: 50%; top: 0; transform-origin: left bottom; display: flex; flex-direction: column; align-items: center; justify-content: center; } .prize-image { width: 60rpx; height: 60rpx; margin-bottom: 10rpx; } .prize-name { font-size: 24rpx; color: white; text-align: center; } .lottery-btn { margin-top: 40rpx; padding: 20rpx 60rpx; background: linear-gradient(45deg, #ff6b6b, #ffa726); color: white; border-radius: 50rpx; font-size: 32rpx; font-weight: bold; } .lottery-btn.disabled { background: #ccc; pointer-events: none; } </style>

3.3 接口请求封装

请求封装(request.js):

import { getToken, refreshToken } from '@/utils/auth' const BASE_URL = 'https://your-api-domain.com/api' // 请求队列,用于处理token刷新时的请求重发 let requestQueue = [] let isRefreshing = false const request = (options) => { return new Promise((resolve, reject) => { const token = getToken() const requestTask = uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data, header: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '', ...options.header }, success: (res) => { if (res.statusCode === 200) { if (res.data.code === 0) { resolve(res.data) } else if (res.data.code === 401) { // token过期,加入队列等待刷新 if (!isRefreshing) { isRefreshing = true handleTokenRefresh().finally(() => { isRefreshing = false // 重发队列中的请求 requestQueue.forEach(cb => cb()) requestQueue = [] }) } // 将当前请求加入队列 requestQueue.push(() => { request(options).then(resolve).catch(reject) }) } else { uni.showToast({ title: res.data.message || '请求失败', icon: 'none' }) reject(res.data) } } else { uni.showToast({ title: `网络错误: ${res.statusCode}`, icon: 'none' }) reject(res) } }, fail: (error) => { uni.showToast({ title: '网络连接失败', icon: 'none' }) reject(error) } }) // 返回requestTask用于取消请求 return requestTask }) } const handleTokenRefresh = async () => { try { const token = getToken() if (!token) { // 跳转到登录页 uni.navigateTo({ url: '/pages/login/login' }) return } const result = await refreshToken() if (result.code === 0) { // 刷新成功,更新token uni.setStorageSync('token', result.data.token) } else { // 刷新失败,跳转登录 uni.navigateTo({ url: '/pages/login/login' }) } } catch (error) { uni.navigateTo({ url: '/pages/login/login' }) } } // 封装常用方法 export const get = (url, data = {}) => request({ url, method: 'GET', data }) export const post = (url, data = {}) => request({ url, method: 'POST', data }) export const put = (url, data = {}) => request({ url, method: 'PUT', data }) export const del = (url, data = {}) => request({ url, method: 'DELETE', data }) export default request

奖池接口封装(pool.js):

import { get, post } from '@/api/request' export const getPrizePools = (params) => { return get('/prize-pools', params) } export const getPoolDetail = (id) => { return get(`/prize-pools/${id}`) } export const getPoolPrizes = (poolId) => { return get(`/prize-pools/${poolId}/prizes`) } export const startLottery = (poolId) => { return post('/lottery/start', { pool_id: poolId }) } export const confirmLottery = (lotteryId) => { return post('/lottery/confirm', { lottery_id: lotteryId }) } export const getLotteryHistory = (params) => { return get('/lottery/history', params) }

4. 后端API开发

4.1 服务器架构设计

server/ ├── app.js # 应用入口 ├── config/ # 配置文件 │ └── database.js # 数据库配置 ├── routes/ # 路由文件 │ ├── auth.js # 认证路由 │ ├── pools.js # 奖池路由 │ ├── lottery.js # 抽奖路由 │ └── orders.js # 订单路由 ├── controllers/ # 控制器 │ ├── authController.js │ ├── poolController.js │ ├── lotteryController.js │ └── orderController.js ├── models/ # 数据模型 │ ├── User.js │ ├── PrizePool.js │ ├── Prize.js │ └── Order.js ├── middleware/ # 中间件 │ ├── auth.js # 认证中间件 │ └── validation.js # 参数验证 └── utils/ # 工具函数 ├── jwt.js # JWT工具 └── response.js # 响应封装

4.2 核心业务逻辑实现

抽奖算法实现(lotteryController.js):

const { PrizePool, Prize, Order, User } = require('../models') const { AppError } = require('../utils/response') class LotteryController { // 开始抽奖 async startLottery(req, res, next) { try { const { pool_id } = req.body const user_id = req.user.id // 验证奖池状态 const pool = await PrizePool.findByPk(pool_id, { include: [{ model: Prize, as: 'prizes' }] }) if (!pool || pool.status !== 'active') { throw new AppError('奖池不存在或已结束', 400) } if (pool.remaining_tickets <= 0) { throw new AppError('奖池已抽完', 400) } // 验证用户余额 const user = await User.findByPk(user_id) if (user.balance < pool.price) { throw new AppError('余额不足', 400) } // 执行抽奖算法 const prize = await this.calculatePrize(pool) // 创建订单 const order = await Order.create({ order_no: this.generateOrderNo(), user_id, pool_id, amount: pool.price, prize_id: prize.id, status: 'paid' }) // 更新库存 await pool.decrement('remaining_tickets') await prize.decrement('remaining_stock') await user.decrement('balance', { by: pool.price }) // 记录抽奖日志 await this.recordLotteryLog(user_id, pool_id, prize.id, order.id) res.json({ code: 0, data: { order_id: order.id, prize: { id: prize.id, name: prize.name, level: prize.level, image: prize.image_url } } }) } catch (error) { next(error) } } // 计算中奖奖品 async calculatePrize(pool) { const prizes = pool.prizes const totalProbability = prizes.reduce((sum, prize) => sum + parseFloat(prize.probability), 0) // 验证概率总和 if (Math.abs(totalProbability - 1) > 0.0001) { throw new AppError('奖池概率配置错误', 500) } // 生成随机数 const random = Math.random() let currentProbability = 0 for (const prize of prizes) { currentProbability += parseFloat(prize.probability) if (random <= currentProbability && prize.remaining_stock > 0) { return prize } } // 如果所有奖品库存为0,返回最后一个奖品(通常为谢谢参与) return prizes[prizes.length - 1] } // 生成订单号 generateOrderNo() { const timestamp = new Date().getTime() const random = Math.random().toString().slice(2, 8) return `ORD${timestamp}${random}` } // 记录抽奖日志 async recordLotteryLog(userId, poolId, prizeId, orderId) { // 实现抽奖日志记录逻辑 // 这里可以记录到专门的抽奖日志表 } } module.exports = new LotteryController()

JWT认证中间件(auth.js):

const jwt = require('jsonwebtoken') const { User } = require('../models') const { AppError } = require('../utils/response') const auth = async (req, res, next) => { try { const token = req.header('Authorization')?.replace('Bearer ', '') if (!token) { throw new AppError('访问令牌缺失', 401) } const decoded = jwt.verify(token, process.env.JWT_SECRET) const user = await User.findByPk(decoded.userId) if (!user) { throw new AppError('用户不存在', 401) } req.user = user next() } catch (error) { if (error.name === 'JsonWebTokenError') { next(new AppError('无效的访问令牌', 401)) } else if (error.name === 'TokenExpiredError') { next(new AppError('访问令牌已过期', 401)) } else { next(error) } } } module.exports = auth

5. 后台管理系统开发

5.1 管理端功能规划

后台管理系统需要包含以下核心模块:

  • 奖池管理:创建、编辑、上下架奖池
  • 奖品管理:配置奖品信息、库存、概率
  • 订单管理:查看订单、发货处理
  • 用户管理:用户信息、余额管理
  • 数据统计:销售数据、中奖统计

5.2 奖池管理实现

<template> <div class="pool-management"> <el-card> <template #header> <div class="card-header"> <span>奖池管理</span> <el-button type="primary" @click="handleCreate">新增奖池</el-button> </div> </template> <el-table :data="poolList" v-loading="loading"> <el-table-column prop="id" label="ID" width="80" /> <el-table-column prop="name" label="奖池名称" /> <el-table-column prop="price" label="价格" width="120"> <template #default="{ row }"> ¥{{ row.price }} </template> </el-table-column> <el-table-column prop="total_tickets" label="总票数" width="100" /> <el-table-column prop="remaining_tickets" label="剩余票数" width="100" /> <el-table-column prop="status" label="状态" width="100"> <template #default="{ row }"> <el-tag :type="row.status === 'active' ? 'success' : 'info'"> {{ row.status === 'active' ? '进行中' : '已结束' }} </el-tag> </template> </el-table-column> <el-table-column label="操作" width="200"> <template #default="{ row }"> <el-button size="small" @click="handleEdit(row)">编辑</el-button> <el-button size="small" @click="handlePrizeManage(row)">奖品管理</el-button> <el-button size="small" :type="row.status === 'active' ? 'warning' : 'success'" @click="handleToggleStatus(row)" > {{ row.status === 'active' ? '结束' : '开启' }} </el-button> </template> </el-table-column> </el-table> <el-pagination v-model:current-page="pagination.current" v-model:page-size="pagination.size" :total="pagination.total" @current-change="loadPools" layout="total, prev, pager, next" /> </el-card> <!-- 奖池编辑对话框 --> <el-dialog v-model="dialogVisible" :title="dialogTitle"> <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="price"> <el-input-number v-model="form.price" :min="1" :precision="2" /> </el-form-item> <el-form-item label="总票数" prop="total_tickets"> <el-input-number v-model="form.total_tickets" :min="1" /> </el-form-item> <el-form-item label="描述" prop="description"> <el-input v-model="form.description" type="textarea" :rows="3" /> </el-form-item> </el-form> <template #footer> <el-button @click="dialogVisible = false">取消</el-button> <el-button type="primary" @click="handleSubmit">确定</el-button> </template> </el-dialog> </div> </template> <script setup> import { ref, reactive, onMounted, computed } from 'vue' import { ElMessage, ElMessageBox } from 'element-plus' import { getPrizePools, createPrizePool, updatePrizePool, togglePoolStatus } from '@/api/admin/pool' const loading = ref(false) const dialogVisible = ref(false) const formRef = ref() const isEditing = ref(false) const poolList = ref([]) const pagination = reactive({ current: 1, size: 10, total: 0 }) const form = reactive({ id: null, name: '', price: 0, total_tickets: 0, description: '' }) const rules = { name: [{ required: true, message: '请输入奖池名称', trigger: 'blur' }], price: [{ required: true, message: '请输入价格', trigger: 'blur' }], total_tickets: [{ required: true, message: '请输入总票数', trigger: 'blur' }] } const dialogTitle = computed(() => isEditing.value ? '编辑奖池' : '新增奖池') onMounted(() => { loadPools() }) const loadPools = async () => { loading.value = true try { const res = await getPrizePools({ page: pagination.current, limit: pagination.size }) poolList.value = res.data.list pagination.total = res.data.total } catch (error) { ElMessage.error('加载失败') } finally { loading.value = false } } const handleCreate = () => { isEditing.value = false Object.keys(form).forEach(key => { if (key !== 'id') form[key] = '' }) dialogVisible.value = true } const handleEdit = (row) => { isEditing.value = true Object.keys(form).forEach(key => { form[key] = row[key] }) dialogVisible.value = true } const handleSubmit = async () => { if (!formRef.value) return try { await formRef.value.validate() if (isEditing.value) { await updatePrizePool(form.id, form) ElMessage.success('更新成功') } else { await createPrizePool(form) ElMessage.success('创建成功') } dialogVisible.value = false loadPools() } catch (error) { ElMessage.error('操作失败') } } const handleToggleStatus = async (row) => { try { await ElMessageBox.confirm( `确定要${row.status === 'active' ? '结束' : '开启'}这个奖池吗?`, '提示', { type: 'warning' } ) await togglePoolStatus(row.id, row.status === 'active' ? 'inactive' : 'active') ElMessage.success('操作成功') loadPools() } catch (error) { // 用户取消操作 } } const handlePrizeManage = (row) => { // 跳转到奖品管理页面 window.open(`/admin/prize-management?pool_id=${row.id}`, '_blank') } </script> <style scoped> .card-header { display: flex; justify-content: space-between; align-items: center; } </style>

6. 接口文档设计

6.1 API文档结构

采用OpenAPI 3.0规范编写接口文档:

openapi: 3.0.0 info: title: 一番赏商城API version: 1.0.0 description: 一番赏手办商城小程序后端API接口文档 servers: - url: https://api.ichiban-mall.com description: 生产环境 paths: /api/prize-pools: get: tags: [奖池] summary: 获取奖池列表 parameters: - name: page in: query schema: type: integer default: 1 - name: limit in: query schema: type: integer default: 10 responses: '200': description: 成功 content: application/json: schema: type: object properties: code: type: integer example: 0 data: type: object properties: list: type: array items: $ref: '#/components/schemas/PrizePool' total: type: integer example: 100 /api/lottery/start: post: tags: [抽奖] summary: 开始抽奖 security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object properties: pool_id: type: integer example: 1 responses: '200': description: 抽奖成功 content: application/json: schema: type: object properties: code: type: integer example: 0 data: type: object properties: order_id: type: integer example: 1001 prize: $ref: '#/components/schemas/Prize' components: schemas: PrizePool: type: object properties: id: type: integer example: 1 name: type: string example: "海贼王一番赏" price: type: number example: 50.00 total_tickets: type: integer example: 1000 remaining_tickets: type: integer example: 500 status: type: string enum: [active, inactive] example: "active" Prize: type: object properties: id: type: integer example: 1 name: type: string example: "A赏-路飞手办" level: type: string enum: [A, B, C, D, E, F, G] example: "A" image_url: type: string example: "https://example.com/prize.jpg" securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT

6.2 接口测试示例

使用Postman进行接口测试:

// 获取奖池列表 const getPools = async () => { const response = await fetch('https://api.ichiban-mall.com/api/prize-pools?page=1&limit=10', { method: 'GET', headers: { 'Content-Type': 'application/json' } }) const result = await response.json() console.log('奖池列表:', result) } // 开始抽奖 const startLottery = async (poolId, token) => { const response = await fetch('https://api.ichiban-mall.com/api/lottery/start', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ pool_id: poolId }) }) const result = await response.json() console.log('抽奖结果:', result) }

7. 部署与运维

7.1 生产环境部署

服务器配置:

# 安装PM2进程管理 npm install pm2 -g # 创建PM2配置文件 ecosystem.config.js module.exports = { apps: [{ name: 'ichiban-server', script: './app.js', instances: 'max', exec_mode: 'cluster', env: { NODE_ENV: 'production', PORT: 3000 }, error_file: './logs/err.log', out_file: './logs/out.log', log_file: './logs/combined.log', time: true }] } # 启动服务 pm2 start ecosystem.config.js

Nginx配置:

server { listen 80; server_name api.ichiban-mall.com; location / { proxy_pass http://localhost:3000; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme