从小程序到 WebView:一个理发店管理系统的全栈架构设计与落地实践
本文首发于 CSDN
阅读提示:这是一个真实上线运行的理发店管理系统,覆盖微信小程序、H5 手机端、WebView APK 三种前端形态,后端基于 Node.js + Express + MySQL,并集成了 DeepSeek AI 经营分析。全文约 6000 字,建议收藏后对照阅读。
一、项目背景与诉求
一家线下理发店在日常经营中面临几个典型痛点:
- 收银混乱:项目多、价格杂,理发师提成靠手工记账,月底对账容易出错;
- 会员流失:没有会员储值、积分、等级体系,老客沉淀不下来;
- 生意洞察缺失:老板不知道哪个时段最忙、哪个项目最赚钱、哪位理发师贡献最大;
- 跨端触达难:顾客希望手机上能预约、查看余额,老板希望随时随地看生意、收通知。
于是我们规划了一个**「老板 + 理发师 + 顾客」三方角色的闭环系统,并且要求一套后端、多种前端**可复用。
最终交付的形态是:
| 端 | 技术形态 | 面向对象 | 作用 |
|---|---|---|---|
| 微信小程序 | 原生小程序 | 顾客 / 老板 | 下单、预约、收银、会员管理 |
| H5 手机端 | 单页应用(SPA) | 顾客 / 老板 / 理发师 | WebView 内嵌、跨端兼容 |
| Android APK | Capacitor 打包的 WebView 应用 | 顾客 / 老板 | 应用商店分发、离线兜底 |
| 后端服务 | Node.js (Express) | 所有端 | 统一业务 API + 数据 |
二、技术选型与理由
后端
| 组件 | 选型 | 理由 |
|---|---|---|
| 运行环境 | Node.js | 与前端同为 JS,前后端统一语言,团队上手快 |
| 框架 | Express | 轻量、生态成熟,中间件模型清晰 |
| ORM | Sequelize | 迁移友好、关联关系声明式、事务支持完善 |
| 数据库 | MySQL | 关系型数据天然契合订单/会员/账目场景,事务可靠 |
| 认证 | JWT | 前后端分离、跨端无 Session 问题 |
| 密码 | bcryptjs | 不可逆加盐哈希,安全性有保障 |
| 限流 | express-rate-limit | 防暴力破解、防恶意刷接口 |
| 工具 | dayjs / axios / multer / cors / dotenv | 常用工具链 |
前端(H5 / APK)
- 纯原生 JS 实现的 单页应用(SPA) ,
index.html + style.css + app.js三文件即可运行; - 通过 Capacitor 打包为 Android APK,本质是 WebView 外壳;
- 采用 「远程加载」 架构:APK 启动时加载服务器上的
http://域名/web/,因此后端改一次前端代码,所有已安装的 APK 无需更新即可生效。
架构图
┌─────────────────────────────────────┐
微信小程序 ──────► │ 小程序端 (pages/boss/*, client/*) │
H5/WebView APK ──► │ 前端 SPA (public/web) │──► HTTP/JSON ──► Express 后端 ──► MySQL
(理发师端) ──────► │ │ │ │
└─────────────────────────────────────┘ │ │
▼ ▼
DeepSeek AI PushPlus 微信推送
(经营分析) (新订单通知)
三、系统架构与模块划分
后端采用经典的三层结构:路由层 → 控制器层 → 服务层 → 数据模型层。
server/
├── app.js # 入口:中间件、路由挂载、健康检查、启动
├── config/ # 配置
│ ├── db.js # Sequelize 连接 MySQL
│ ├── jwt.js # JWT 密钥与过期时间
│ ├── wechat.js # 微信小程序/支付配置
│ └── upload.js # 文件上传配置
├── routes/ # 路由(按角色拆分)
│ ├── common.js # 登录、上传、公开设置
│ ├── boss.js # 老板端(管理后台)
│ ├── client.js # 顾客端(小程序)
│ └── barber.js # 理发师端
├── controllers/ # 控制器:处理请求/响应
├── services/ # 服务层:核心业务逻辑
│ ├── AuthService.js # 认证
│ ├── OrderService.js # 订单(含事务、提成、积分)
│ ├── MemberService.js # 会员(充值、积分)
│ ├── AnalyticsService.js# 数据分析
│ ├── NotificationService.js # 微信推送通知
│ └── WechatService.js # 微信登录/手机号解密
├── models/ # Sequelize 数据模型
├── middleware/ # 认证中间件、限流
└── utils/ # 工具:统一响应、会员权益算法、订单号生成
路由如何做权限隔离
用 JWT + 中间件实现"前端不需要知道谁能访问什么,一端一权限":
js
// routes/boss.js ------ 注册若干"无需认证"的接口后,后面全部走管理员认证
router.post('/verify-password', adminPasswordLimiter, bossController.verifyPassword)
router.use(adminAuth) // 此行之后的所有接口都需要管理员 token
router.get('/dashboard', bossController.getDashboard)
// ...
// routes/client.js ------ 所有顾客端接口统一认证
router.use(auth)
router.get('/services', clientController.getServices)
三层认证中间件分工:
auth.js:校验顾客端 JWT(secret);adminAuth.js:校验管理端 JWT(adminSecret),再核对role === 'admin';barberAuth.js:校验理发师 JWT。
密钥与过期时间集中配置(config/jwt.js),客户端 token 默认 30 天、管理端 12 小时,并可在 .env 中覆盖,便于安全轮换。
四、数据库设计(核心表)
系统 11 张表,围绕「订单」流转。最关键的是通过外键关联把顾客、理发师、服务、订单、评价、充值、工资串成一张网。
users 用户/会员 service 服务项目 barbers 理发师
id id id id
openid name name name
role price phone phone
phone duration avatar password
password cost_price introduction
balance commission_rate rating
points status review_count
total_consume password
member_level
关键关联(在 models/index.js 中用 Sequelize 声明):
js
// 服务项目与理发师:多对多(擅长技能)
Barber.belongsToMany(Service, { through: BarberSkill, as: 'skills' })
// 订单的多重归属
Order.belongsTo(User, { foreignKey: 'customer_id', as: 'customer' })
Order.belongsTo(Barber, { foreignKey: 'barber_id', as: 'barber' })
Order.belongsTo(Service, { foreignKey: 'service_id', as: 'service' })
// 工资结算与理发师
SalaryRecord.belongsTo(Barber, { foreignKey: 'barber_id', as: 'barber' })
订单表为何要冗余顾客姓名/理发师姓名?
orders 表允许 customer_id、barber_id 为 null,同时存 customer_name、customer_phone。这是为了支持**「散客下单」**------非会员也能快速收银,并且即便会员/理发师信息后续变动,历史订单仍能完整展示。这是业务上很实用的取舍。
五、核心业务逻辑精讲
5.1 订单创建:事务、折扣、提成、积分四合一
订单创建是整个系统最核心、最容易出错的地方(涉及金额、库存、积分、余额扣减),因此用数据库事务保证原子性:
js
async function createOrder(orderData) {
const transaction = await sequelize.transaction() // 开启事务
try {
const service = await Service.findByPk(service_id, { transaction })
// 1. 会员折扣(黄金85折、铂金75折、钻石65折)
let discount = 0, finalAmount = service.price
if (customer_id) {
const memberDiscount = calcMemberDiscount(customer.member_level)
if (memberDiscount < 1) {
discount = service.price * (1 - memberDiscount)
finalAmount = service.price * memberDiscount
}
}
// 2. 理发师提成 = 实收金额 × 提成率(按折扣后计算,更公平)
const rate = parseFloat(service.commission_rate) || 0
const commissionAmount = Math.round(finalAmount * rate / 100 * 100) / 100
// 3. 余额支付需校验并扣减
if (payment_method === 'balance' || payment_method === 'mixed') {
if (!customer) throw new Error('余额支付必须关联会员')
if (parseFloat(customer.balance) < finalAmount) throw new Error('会员余额不足')
customer.balance = parseFloat(customer.balance) - finalAmount
await customer.save({ transaction })
}
// 4. 创建订单
const order = await Order.create({ ... }, { transaction })
// 5. 会员累计消费、加积分、检查升级
if (customer) {
customer.total_consume += finalAmount
customer.points += calcPoints(finalAmount) // 1元=1积分
customer.member_level = checkMemberUpgrade(customer.total_consume)
await customer.save({ transaction })
}
await transaction.commit() // 全部成功才提交
return order
} catch (err) {
await transaction.rollback() // 任一失败全部回滚
throw err
}
}
设计亮点 :提成按「折扣后实收」而非「原价」计算,避免高折扣项目给理发师发"虚高"提成;订单创建后异步通知老板,且通知失败不阻塞下单流程(
notifyNewOrder(...).catch(() => {}))。
5.2 会员等级与权益算法
用一份配置驱动(utils/memberHelper.js),清晰可维护:
js
const MEMBER_DISCOUNTS = { '普通会员': 1.00, '黄金会员': 0.85, '铂金会员': 0.75, '钻石会员': 0.65 }
const MEMBER_LEVELS = [
{ level: '普通会员', minConsume: 0 },
{ level: '黄金会员', minConsume: 1000 },
{ level: '铂金会员', minConsume: 5000 },
{ level: '钻石会员', minConsume: 15000 },
]
const DEFAULT_RECHARGE_RULES = [
{ min: 100, bonus: 10 }, { min: 200, bonus: 30 },
{ min: 500, bonus: 100 }, { min: 1000, bonus: 300 }, { min: 2000, bonus: 800 },
]
充值时自动匹配赠送金额(从高到低匹配最大档),消费后自动按累计金额升级,形成「充值→消费→升级→再充值」的运营飞轮。
5.3 全维度数据分析
AnalyticsService 提供了 营收、时段、客户类型、服务项目、利润、客户画像、会员、员工、反馈 等 12+ 维度的分析接口。示例:按时段营收占比(上午/下午/晚上):
js
async function getRevenueByTimeSlot(days = 30) {
const orders = await Order.findAll({ where: { status: 'completed', payment_status: 'paid', ... } })
const slots = { morning: 0, afternoon: 0, evening: 0 }
orders.forEach(o => {
const hour = dayjs(o.created_at).hour()
if (hour < 12) slots.morning += o.final_amount
else if (hour < 18) slots.afternoon += o.final_amount
else slots.evening += o.final_amount
})
return { morning: {...}, afternoon: {...}, evening: {...} } // 含占比 percent
}
前端管理后台把这些聚合到「数据分析中心」,老板一眼就能看出哪段时间该加人手、哪个项目该主推。
5.4 AI 经营分析(接入 DeepSeek)
这是项目的差异化亮点:把上面的统计数据喂给 DeepSeek ,生成自然语言的经营建议和会员分析。同时做了月度限流防止 API 费用失控:
js
const AI_MONTHLY_LIMIT = 10 // 每月最多 10 次
// 用 settings 表记录当月使用次数:ai_usage_YYYYMM
async function callDeepSeek(apiKey, systemPrompt, userMessage) {
const response = await fetch('https://api.deepseek.com/v1/chat/completions', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + apiKey },
body: JSON.stringify({ model: 'deepseek-chat', messages: [...], temperature: 0.7, max_tokens: 2000 })
})
return data.choices[0].message.content
}
AI Key 存储在
settings表(deepseek_api_key),月度用量也存储在settings(ai_usage_YYYYMM),用一张通用 KV 表解决"配置 + 计数器"两个需求,无需新建表。
5.5 新订单微信推送通知
通过 PushPlus 实现「新订单 → 老板微信实时提醒」,支持单推(token)和群推(topic)两种模式:
js
// 异步发送,失败不阻塞下单
NotificationService.notifyNewOrder(result).catch(() => {})
// 内部构造 HTML 内容,调用 pushplus 接口
const payload = { token, title: '理发店新订单', content, template: 'html' }
if (topic) payload.topic = topic
axios.post(PUSHPLUS_URL, payload, { timeout: 8000 })
六、微信生态集成
系统同时支持微信小程序,因此接入了微信登录和手机号获取:
js
// code 换 openid
const res = await axios.get('https://api.weixin.qq.com/sns/jscode2session', {
params: { appid, secret, js_code: code, grant_type: 'authorization_code' }
})
// 新版接口直接拿手机号
const res = await axios.post('https://api.weixin.qq.com/wxa/business/getuserphonenumber', { code }, {
params: { access_token }
})
还预留了 微信支付 V3 服务(WechatPayService),实现了 RSA-SHA256 签名、Authorization 头构造、回调验签等,可扩展线上支付。
安全细节 :
WECHAT_APP_SECRET从.env读取而非硬编码;若未配置,微信登录会回退到 mock openid,保证在开发环境能跑通。
七、部署架构:公网穿透 + 反向代理 + 进程守护
这是本项目非常实用的一环------一台运行在树莓派(Orange Pi Zero 3,ARM64) 上的低成本服务器,对外提供服务:
公网用户 ──► frp.yb10032.com:20003 ──► frp 隧道 ──► Orange Pi 本地 :80 (nginx)
│
├── /api/ ──► localhost:3000 (Node)
├── /web/ ──► localhost:3000 (Node 静态)
└── /uploads/──► 服务器磁盘上传目录
- 内网穿透(frp) :服务器在用户内网,无公网 IP,通过 frpc 把公网端口
20003映射到本机80,实现"家里/店里的树莓派 = 阿里云服务器"的效果,零服务器成本; - 反向代理(nginx):天然帮 Node 处理静态资源、gzip、跨域代理;
- 进程守护(PM2) :
pm2 start app.js --name barber-shop,崩溃自动重启,开机自启;
bash
pm2 restart barber-shop # 改完代码重启
⚠️ nginx 里必须配置
proxy_set_header X-Forwarded-For,并让 Express 开启app.set('trust proxy', 1),否则express-rate-limit拿到的全是代理 IP,限流会误伤所有用户。
八、踩坑与优化实录
在真实落地过程中,几个问题比较典型:
8.1 WebView 架构下的"热更新"优势与坑
用 Capacitor 打包时选择远程加载:
json
// capacitor.config.json
{ "server": { "url": "http://frp.yb10032.com:20003/web/", "cleartext": true } }
优势 :改后端一处、所有已安装 APK 即刻生效;APK 内的 assets/public 只是离线兜底。
坑 :cleartext: true 用明文 HTTP,若走公网建议上 HTTPS + androidScheme: "https"。
8.2 "当前余额永远 0.00" ------ 前后端字段不一致
这是接手的真实 bug:后端 /boss/members/:id 返回 { member: {...}, recentOrders, recharges },前端却直接读顶层 member.balance,于是永远是 undefined || '0.00'。
js
// 错误写法
<div>当前余额 ¥${member.balance}</div>
// 正确写法
<div>当前余额 ¥${(member.member && member.member.balance) || '0.00'}</div>
教训:接口返回结构一旦从"扁平"改成"嵌套",前端所有引用点必须同步,建议用 TypeScript/接口文档约束。
8.3 token 过期后用户"卡死"
JWT 过期后,前端只 toast('令牌已过期') 却不会自动登出,用户困在已登录态无法重新登录。修复方式是所有请求封装里统一检测"令牌"关键字并自动登出:
js
} catch(err) {
this.toast(err.message)
if (String(err.message).includes('令牌')) setTimeout(() => this.logout(), 0) // 自动跳登录页
throw err
}
同时把客户端 token 有效期从 7 天放宽到 30 天,管理端放宽到 12 小时,平衡安全与体验。
8.4 JWT 密钥要支持环境变量轮换
密钥写成 process.env.JWT_SECRET || '默认值',避免硬编码;一旦泄露,改 .env 即可让所有旧 token 失效,无需改代码。
九、不足与后续规划
目前系统已能稳定支撑理发店的日常经营,但仍有优化空间:
- 短信验证码:当前是内存缓存 + 调试模式(验证码直接前端返回),生产环境需接入阿里云/腾讯云短信;
- 支付链路:微信/支付宝支付 V3 已预留,需补充真实商户号与回调处理;
- WebView 性能:H5 首屏可考虑加骨架屏、拆包、Service Worker 缓存;
- 管理后台:目前是移动端 H5 页面,后续可做 PC 端管理界面。
十、总结
这个项目麻雀虽小、五脏俱全:从微信小程序到 WebView APK,从 JWT 认证到角色权限隔离,从订单事务到会员等级算法,从营收分析到 AI 经营建议,从 nginx 反代到 frp 内网穿透,几乎覆盖了小规模 Web 系统的所有关键环节。
最有价值的三点经验:
- 架构分层要清晰:路由/控制器/服务/模型分层,业务逻辑抽到 service,便于复用和测试;
- 事务与幂等先行:涉及金额、积分、库存一律上事务,宁可多花几行代码也要保数据一致;
- 站在业务方思考:散客下单、提成按实收算、AI 生成经营建议......真正解决问题的是对业务的理解,而不是炫技。
希望这篇实战复盘能给同样在做中小门店数字化的朋友一些参考。如果你对其中某一部分(如 Capacitor 打包、DeepSeek 接入、frp 内网穿透)感兴趣,欢迎评论区交流。
全套源码、部署脚本欢迎私信交流。感谢阅读!
(本文由真实上线项目整理,技术栈:Node.js 18 + Express 4 + Sequelize 6 + MySQL 8 + JWT + Capacitor + DeepSeek AI,部署于 Orange Pi Zero 3 + nginx + PM2 + frp。)