从零搭建餐饮点餐+外卖小程序(附代码):UniApp 多端源码编译 + Nginx 反向代理
2026年的本地生活服务赛道,餐饮数字化已经从“可选配置”变成了商家的“刚需基建”。对于中小餐饮品牌、县域创业者而言,搭建一套同时覆盖堂食点餐、外卖配送的小程序系统,不再需要投入过多来建多端开发团队——基于最新的UniApp跨端技术栈,搭配Nginx高性能反向代理架构,仅两周就能完成从0到1的全流程落地。
技术栈说明
- 源码与演示:c.ymzan.top
- 前端(用户端小程序+H5):UniApp 3 × Vue 3 × Vite × TypeScript × Pinia × uView Plus
- 商家管理后台:Vue 3 + Element Plus(简略提及)
- 后端 API:NestJS 10+ × TypeORM × MySQL 8.0 × Redis 7
- 部署:Docker / docker-compose + Nginx 1.25+ 反向代理 + HTTPS(Let’s Encrypt)
- 第三方:微信小程序登录 / 微信 JSAPI / 腾讯地图 LBS
系统架构总览
┌──────────────────────┐
│ 微信小程序 / H5 │ ← UniApp 编译
└──────────┬───────────┘
│ HTTPS
┌──────────▼───────────┐
│ Nginx (80/443) │
│ ┌──────────────────┐ │
│ │ /api/* → NestJS │ │ ← 反向代理
│ │ /admin → Vue SPA │ │
│ │ /upload → 静态 │ │
│ └──────────────────┘ │
└──────────┬───────────┘
┌────────────────┼────────────────┐
┌──────▼──────┐ ┌─────▼─────┐ ┌──────▼──────┐
│ NestJS API │ │ MySQL 8.0 │ │ Redis 7.0 │
│ :3000 │ │ 主从可选 │ │ 缓存/锁/Session│
└─────────────┘ └───────────┘ └─────────────┘
数据库设计(核心表)
-- 字符集统一 utf8mb4
CREATE DATABASE food_order
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE food_order;
-- 菜品分类
CREATE TABLE category (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(30) NOT NULL,
sort INT DEFAULT 0,
enabled TINYINT(1) DEFAULT 1
);
-- 菜品
CREATE TABLE dish (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
category_id INT NOT NULL,
name VARCHAR(50) NOT NULL,
price DECIMAL(10,2) NOT NULL,
image VARCHAR(255),
description VARCHAR(200),
stock INT DEFAULT 999,
enabled TINYINT(1) DEFAULT 1,
INDEX idx_category (category_id)
);
-- 用户
CREATE TABLE `user` (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
openid VARCHAR(64) UNIQUE,
phone VARCHAR(20),
nickname VARCHAR(50),
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 订单
CREATE TABLE `order` (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_no VARCHAR(32) UNIQUE NOT NULL,
user_id BIGINT NOT NULL,
total_amount DECIMAL(10,2) NOT NULL,
status TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2配送中 3完成 4取消',
address JSON,
pay_type TINYINT DEFAULT 1 COMMENT '1微信 2余额',
paid_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user (user_id),
INDEX idx_status (status)
);
-- 订单明细
CREATE TABLE order_item (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
dish_id BIGINT NOT NULL,
dish_name VARCHAR(50),
price DECIMAL(10,2),
quantity INT,
INDEX idx_order (order_id)
);
后端 —— NestJS 10 + TypeORM + Redis
1. 初始化项目
npm i -g @nestjs/cli
nest new food-api
cd food-api
npm i @nestjs/typeorm typeorm mysql2 redis @nestjs/config
npm i class-validator class-transformer
2. 全局配置(src/app.module.ts 简化示例)
// src/app.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { TypeOrmModule } from '@nestjs/typeorm';
import { UserModule } from './user/user.module';
import { DishModule } from './dish/dish.module';
import { OrderModule } from './order/order.module';
@Module({
imports: [
ConfigModule.forRoot({ isGlobal: true }),
TypeOrmModule.forRootAsync({
inject: [ConfigService],
useFactory: (c: ConfigService) => ({
type: 'mysql',
host: c.get('DB_HOST'),
port: 3306,
username: c.get('DB_USER'),
password: c.get('DB_PASS'),
database: c.get('DB_NAME'),
entities: [__dirname + '/**/*.entity{.ts,.js}'],
synchronize: false,
logging: false,
}),
}),
UserModule,
DishModule,
OrderModule,
],
})
export class AppModule {}
.env
DB_HOST=127.0.0.1
DB_USER=root
DB_PASS=123456
DB_NAME=food_order
JWT_SECRET=food_order_secret_2026
WECHAT_APPID=wxxxxxxxxx
WECHAT_SECRET=xxxxxxxx
3. 菜品模块(示例 Entity + Controller)
// src/dish/dish.entity.ts
import { Entity, PrimaryGeneratedColumn, Column } from 'typeorm';
@Entity('dish')
export class Dish {
@PrimaryGeneratedColumn()
id: number;
@Column()
categoryId: number;
@Column()
name: string;
@Column('decimal', { precision: 10, scale: 2 })
price: number;
@Column({ nullable: true })
image: string;
@Column({ default: 1 })
enabled: boolean;
}
// src/dish/dish.controller.ts
import { Controller, Get, Query } from '@nestjs/common';
import { DishService } from './dish.service';
@Controller('api/dishes')
export class DishController {
constructor(private readonly svc: DishService) {}
@Get()
list(@Query('categoryId') categoryId?: string) {
return this.svc.findByCategory(+categoryId);
}
}
4. 全局前缀 & CORS & ValidationPipe(main.ts)
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.setGlobalPrefix('api');
app.enableCors();
app.useGlobalPipes(
new ValidationPipe({ whitelist: true, transform: true }),
);
await app.listen(3000);
}
bootstrap();
UniApp 前端(Vue 3 + Vite + TypeScript + Pinia)
1. 创建项目
使用 HBuilderX「新建 UniApp Vue3+TS 模板」或命令行:
# 若已安装 @dcloudio/uvue-cli
npx degit dcloudio/uni-preset-vue#vite-ts food-miniapp
cd food-miniapp
pnpm install
pnpm add pinia @uni-helper/pinia-plugin-uni pinia-plugin-persistedstate
pnpm add unplugin-auto-import -D
2. Pinia 配置(src/stores/cart.ts — 购物车持久化)
// src/stores/cart.ts
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useCartStore = defineStore('cart', () => {
const items = ref<Array<{
dishId: number;
name: string;
price: number;
qty: number;
}>>([]);
const totalPrice = computed(() =>
items.value.reduce((s, i) => s + i.price * i.qty, 0),
);
function add(dish: { dishId: number; name: string; price: number }) {
const exist = items.value.find(i => i.dishId === dish.dishId);
exist ? exist.qty++ : items.value.push({ ...dish, qty: 1 });
}
function clear() { items.value = []; }
return { items, totalPrice, add, clear };
}, {
persist: { storage: { getItem: uni.getStorageSync, setItem: uni.setStorageSync } },
});
3. HTTP 请求封装(src/utils/request.ts)
// src/utils/request.ts
const BASE_URL = import.meta.env.VITE_API_BASE as string;
export function request<T>(options: {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
data?: any;
header?: Record<string, string>;
}): Promise<T> {
return new Promise((resolve, reject) => {
uni.request({
url: BASE_URL + options.url,
method: options.method ?? 'GET',
data: options.data,
header: {
'Content-Type': 'application/json',
Authorization: uni.getStorageSync('token')
? 'Bearer ' + uni.getStorageSync('token')
: '',
...options.header,
},
success: res => {
if (res.statusCode === 200) resolve(res.data as T);
else reject(res);
},
fail: reject,
});
});
}
.env.development
VITE_API_BASE=http://localhost:3000
.env.production
VITE_API_BASE=https://api.yourdomain.com
4. 菜品列表页(pages/menu/menu.vue 简化)
<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { request } from '@/utils/request';
import { useCartStore } from '@/stores/cart';
const dishes = ref<any[]>([]);
const cart = useCartStore();
onMounted(async () => {
dishes.value = await request<any[]>('/api/dishes?categoryId=1');
});
function handleAdd(d: any) {
cart.add({ dishId: d.id, name: d.name, price: d.price });
}
</script>
<template>
<view class="menu">
<view v-for="d in dishes" :key="d.id" class="card">
<image :src="d.image" mode="aspectFill" />
<text>{{ d.name }}</text>
<text class="price">¥{{ d.price }}</text>
<button size="mini" @click="handleAdd(d)">加购</button>
</view>
</view>
</template>
5. 微信小程序登录(获取 openid)
// src/utils/wx-auth.ts
import { request } from './request';
export function wxLogin() {
uni.login({
provider: 'weixin',
success: async (res) => {
// 调后端 code2session 接口
const data = await request<{ token: string; openid: string }>({
url: '/api/auth/wx-login',
method: 'POST',
data: { code: res.code },
});
uni.setStorageSync('token', data.token);
},
});
}
后端 NestJS 伪代码(AuthService):
async wxLogin(code: string) {
const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${this.appid}&secret=${this.secret}&js_code=${code}&grant_type=authorization_code`;
const res = await firstValueFrom(this.http.get(url));
// 查找/创建用户 → 签发 JWT
return { token: this.jwtService.sign({ openid: res.openid }) };
}
多端编译说明
# H5
pnpm run dev:h5 # 本地 http://localhost:5173
pnpm run build:h5 # 产出 dist/build/h5/
# 微信小程序(需用 HBuilderX 或 CLI 导入微信开发者工具)
pnpm run dev:mp-weixin # 产出 dist/dev/mp-weixin/
pnpm run build:mp-weixin
UniApp 条件编译示例(区分微信小程序与 H5 支付):
<!-- #ifdef MP-WEIXIN -->
<button @click="wxPay">微信支付</button>
<!-- #endif -->
<!-- #ifdef H5 -->
<button @click="h5Pay">H5 支付</button>
<!-- #endif -->
Docker 容器化
docker-compose.yml
version: '3.9'
services:
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: food_order
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
redis:
image: redis:7-alpine
ports:
- "6379:6379"
api:
build: ./food-api
ports:
- "3000:3000"
env_file:
- ./food-api/.env
depends_on:
- mysql
- redis
nginx:
image: nginx:1.25-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./dist/h5:/usr/share/nginx/html/h5
- ./ssl:/etc/nginx/ssl
depends_on:
- api
volumes:
mysql_data:
NestJS Dockerfile(多阶段构建):
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/.env ./
CMD ["node", "dist/main"]
Nginx 反向代理 + HTTPS 配置
nginx/conf.d/food.conf
server {
listen 80;
server_name api.yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
server_name api.yourdomain.com;
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
# ===== API 反向代理到 NestJS =====
location /api/ {
proxy_pass http://api:3000/api/;
proxy_http_version 1.1;
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 https;
# 支持 WebSocket(订单推送等)
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
# ===== H5 静态资源(UniApp build:h5 产物)=====
location /h5/ {
alias /usr/share/nginx/html/h5/;
try_files $uri $uri/ /h5/index.html;
}
# ===== 上传静态资源 =====
location /upload/ {
alias /data/uploads/;
}
# 安全头
add_header Strict-Transport-Security "max-age=31536000" always;
}
验证并重载:
docker exec -it food-nginx nginx -t
docker exec -it food-nginx nginx -s reload
八、微信支付 JSAPI 简要流程
- 小程序端调
wx.login()拿 code → 后端换取 openid - 下单时后端调微信统一下单接口(
/pay/unifiedorder),传入trade_type=JSAPI、openid - 后端返回
timeStamp / nonceStr / package / signType / paySign - 小程序调
wx.requestPayment(params)拉起支付 - 微信异步回调
/api/pay/notify→ 验签 → 更新订单状态
⚠️ notify 接口需在 Nginx 中放行 CSRF 校验,且返回
<xml><return_code><![CDATA[SUCCESS]]></return_code></xml>

小结
UniApp(Vue3+Vite+TS) + NestJS + MySQL + Redis + Docker + Nginx 反向代理从零搭建餐饮点餐与外卖小程序的落地,同时覆盖、Android App、iOS App、H5等多个平台,开发成本比传统模式降低70%。后续我们还可以基于这套架构持续优化:通过CDN加速静态资源访问,将用户首屏加载时间进一步压缩到1秒以内;接入Redis缓存热点菜品数据,应对午晚高峰的高并发请求;基于UniApp X的UTS能力开发原生推送插件,实现订单状态实时推送提醒。
对于中小餐饮创业者而言,这套方案完全可以支撑从单店到县域连锁的业务发展,无需投入高昂的开发成本,就能拥有一套完全自主可控的数字化点餐系统。
更多推荐



所有评论(0)