从小程序到 WebView:一个理发店管理系统的全栈架构设计与落地实践

从小程序到 WebView:一个理发店管理系统的全栈架构设计与落地实践

本文首发于 CSDN

阅读提示:这是一个真实上线运行的理发店管理系统,覆盖微信小程序、H5 手机端、WebView APK 三种前端形态,后端基于 Node.js + Express + MySQL,并集成了 DeepSeek AI 经营分析。全文约 6000 字,建议收藏后对照阅读。


一、项目背景与诉求

一家线下理发店在日常经营中面临几个典型痛点:

  1. 收银混乱:项目多、价格杂,理发师提成靠手工记账,月底对账容易出错;
  2. 会员流失:没有会员储值、积分、等级体系,老客沉淀不下来;
  3. 生意洞察缺失:老板不知道哪个时段最忙、哪个项目最赚钱、哪位理发师贡献最大;
  4. 跨端触达难:顾客希望手机上能预约、查看余额,老板希望随时随地看生意、收通知。

于是我们规划了一个**「老板 + 理发师 + 顾客」三方角色的闭环系统,并且要求一套后端、多种前端**可复用。

最终交付的形态是:

技术形态 面向对象 作用
微信小程序 原生小程序 顾客 / 老板 下单、预约、收银、会员管理
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_idbarber_idnull,同时存 customer_namecustomer_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),月度用量也存储在 settingsai_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 系统的所有关键环节。

最有价值的三点经验:

  1. 架构分层要清晰:路由/控制器/服务/模型分层,业务逻辑抽到 service,便于复用和测试;
  2. 事务与幂等先行:涉及金额、积分、库存一律上事务,宁可多花几行代码也要保数据一致;
  3. 站在业务方思考:散客下单、提成按实收算、AI 生成经营建议......真正解决问题的是对业务的理解,而不是炫技。

希望这篇实战复盘能给同样在做中小门店数字化的朋友一些参考。如果你对其中某一部分(如 Capacitor 打包、DeepSeek 接入、frp 内网穿透)感兴趣,欢迎评论区交流。

全套源码、部署脚本欢迎私信交流。感谢阅读!


(本文由真实上线项目整理,技术栈:Node.js 18 + Express 4 + Sequelize 6 + MySQL 8 + JWT + Capacitor + DeepSeek AI,部署于 Orange Pi Zero 3 + nginx + PM2 + frp。)

相关推荐
心易行者15 分钟前
html在线运行搭AI编程验证流水线:5步走完从生成到到上线全流程
人工智能·python·ai编程
LccKyI16 分钟前
C#基础编程 面试题汇总(二)
java·面试·c#
一晌小贪欢20 分钟前
Python办公17:暴力拆分——按页数或指定范围提取 PDF 核心页面
java·开发语言·python·pdf·excel·数据可视化·python办公
2401_8346369921 分钟前
Docker 容器化技术全解:从镜像构建到容器编排的完整教案1
java·docker·容器
青 春 记 忆21 分钟前
零基础入门python31:Django商城第一步——创建项目并跑通第一个请求
python·django·后端开发
孫治AllenSun21 分钟前
【LangChain4J-06】Prompt 工程与模板化开发
开发语言·python·prompt
qq_4260039623 分钟前
多语言新增语种全量测试策略
前端·javascript·python·pycharm·自动化
AuLuo-26 分钟前
SearXNG 物理机部署搭建免费搜索引擎服务
python·搜索引擎
jjjava2.027 分钟前
初步认识接口测试
java·可用性测试