资讯详情

实体门店三端会员系统一致性架构设计

📅 2026/10/30 14:54:24 | 华诺云谱 👁 阅读
实体门店三端会员系统一致性架构设计
简介这是一款面向实体零售场景的全栈式会员营销系统适用于汽车4S店、餐饮、花店、甜品店等线下门店及线上电商解决传统收银与会员运营割裂问题。系统基于Java SpringBootMySQL构建完整覆盖微信小程序前台、H5轻应用、后端API服务及后台管理四端集成优惠券、储值卡、集次卡、电子券、积分体系与支付收款等核心营销能力。资源包共531个文件含422个Java业务逻辑与控制器类、50个XML配置与Mapper映射文件、33个PNG/13个JPG界面资源图以及SQL建表脚本、Shell部署脚本和License说明整体仅5.5MB结构清晰、开箱即用。已有696人学习下载开发者可直接获取完整可运行源码、标准化分层架构Controller-Service-Mapper、典型业务模块实现如CouponServiceImpl、MemberServiceImpl、WeixinServiceImpl及前后端联调范例快速复用或二次开发。1. 实体店铺会员系统为什么90%的“小程序H5后台”项目在第三个月就卡在数据不同步上你刚上线一套「微信小程序H5页面后台管理」的会员系统顾客扫个码就能领券、积分、查余额店员在后台改个折扣前端秒变老板手机点开H5报表昨天哪位会员复购了3次一目了然——听起来很稳但真实情况是小程序里显示积分100H5页面刷新后还是80后台管理端导出的Excel里却是120。三端数据对不上不是bug是架构设计时没把「状态一致性」当第一优先级来压测。这不是小作坊的通病连锁茶饮、社区生鲜、美业门店的SaaS服务商每年都有35个同类项目因会员等级错乱、优惠券重复核销、库存超卖被客户投诉下线。本文不讲概念只拆解一个能跑通100家实体门店的真实路径用统一用户中心兜底身份用事件驱动解耦营销动作用幂等本地缓存服务端校验三层防线守住数据底线。适合正在选型、已启动开发但卡在联调、或刚上线正被老板追问“为什么小程序和H5优惠券不一致”的一线开发者与技术负责人。2. 前端三端小程序/H5/后台管理如何共用同一套用户体系而不翻车实体门店场景下“微信小程序”和“H5页面”本质是两个独立运行环境小程序走微信授权体系H5走浏览器CookieToken后台管理又是另一套RBAC权限系统。强行让三端共享session或硬塞同一套JWT不出三天就会遇到“H5登录后小程序自动登出”“后台改了会员等级H5页面缓存没刷新”这类玄学问题。真正可靠的解法不是统一登录态而是统一用户身份标识状态决策权。2.1 用户中心必须独立部署且只暴露ID映射与基础属性不要把用户表放在订单服务或营销服务里。必须拆出独立的user-center微服务哪怕单体架构也按模块物理隔离它只做三件事绑定微信OpenID / UnionID 与内部member_id主键存储基础属性手机号、昵称、头像、注册时间、最后登录时间提供/v1/member/info?member_idxxx接口返回脱敏后的只读字段提示member_id必须是业务自增ID或Snowflake生成绝不能直接用微信OpenID当主键。原因OpenID在不同公众号/小程序下不互通且微信可能调整规则而member_id是你所有业务的锚点订单、积分、优惠券都关联它。# user-center服务核心逻辑示例FastAPI from fastapi import Depends, HTTPException from sqlalchemy import select async def get_member_info(member_id: str, db: AsyncSession Depends(get_db)): stmt select(Member).where(Member.member_id member_id) result await db.execute(stmt) member result.scalar_one_or_none() if not member: raise HTTPException(status_code404, detailMember not found) # 严格过滤返回字段不暴露敏感信息 return { member_id: member.member_id, nickname: member.nickname or 匿名用户, avatar: member.avatar or https://example.com/default.png, level: member.level, # 会员等级数字非文字 points: member.points, # 当前积分 created_at: member.created_at.isoformat() }这段代码的关键不在语法而在返回字段的克制性不返回手机号除非带权限校验、不返回OpenID前端不需要、不返回密码哈希。所有业务方小程序/H5/后台调用此接口拿用户快照但修改操作必须走各自领域服务——比如小程序调用/marketing/coupon/useH5调用/order/create后台调用/admin/member/update-level它们各自校验权限后再通过内部RPC或消息队列通知user-center更新points或level。2.2 小程序端用UnionID打通多入口避免OpenID碎片化实体门店常同时运营公众号、小程序、视频号小店。如果只存小程序的OpenID用户从公众号菜单进H5就变成新用户。必须在首次授权时用jscode2session拿到unionid需公众号和小程序同主体并以此为依据合并用户// 小程序前端获取unionid逻辑wx.login wx.getUserInfo 合并 wx.login({ success: res { // 1. 用code向后端换取session_key和unionid wx.request({ url: https://api.yourdomain.com/auth/wx-login, method: POST, data: { code: res.code }, success: authRes { const { unionid, openid, session_key } authRes.data; // 2. 如果unionid存在说明该用户已在其他渠道注册过 if (unionid) { // 直接绑定到现有member_id bindToExistingMember(unionid); } else { // 新用户用openid创建member_id createNewMember(openid); } } }); } });注意wx.getUserInfo已废弃必须用wx.login 后端jscode2session。unionid仅在用户关注过公众号或使用过同主体小程序时才返回因此要设计降级逻辑无unionid时先用openid创建临时用户后续用户主动绑定手机号时再合并。2.3 H5端用localStorageToken双保险维持会话但绝不信任前端TokenH5没有微信环境必须自己管登录态。常见错误是把Token存在localStorage里然后每次请求都带上——这会导致XSS攻击时Token被盗。正确做法是登录成功后后端下发access_tokenJWT和refresh_token长时效存HttpOnly Cookie前端将access_token存localStorage用于API请求Headeraccess_token过期如2小时后用refresh_token自动续期无需用户重新登录所有敏感操作如核销优惠券、修改手机号必须二次校验短信验证码或密码// H5前端请求拦截器Axios axios.interceptors.request.use( async config { const token localStorage.getItem(access_token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }, error Promise.reject(error) ); // 刷新Token逻辑单独封装 const refreshToken async () { try { const res await axios.post(/auth/refresh, {}, { withCredentials: true // 携带HttpOnly Cookie }); localStorage.setItem(access_token, res.data.access_token); return res.data.access_token; } catch (e) { // refresh失败强制跳转登录页 window.location.href /login; } };关键点withCredentials: true确保Cookie能发送而access_token只存前端即使被XSS窃取有效期也极短refresh_token存在服务端可控的Cookie里攻击者无法读取。3. 营销活动如何做到“一次配置三端生效”且不引发并发超发门店搞“满100减20”活动小程序首页弹窗、H5活动页轮播、后台管理端实时看核销数——这三处UI可以不同但活动规则、库存、状态必须绝对一致。很多团队用“后台改数据库字段前端定时轮询”来同步结果就是轮询间隔2秒10个用户同时点击第11个用户看到“库存充足”却提示“已抢光”。真正的解法是规则中心化 库存预占 状态广播。3.1 活动配置必须抽象为JSON Schema禁止硬编码规则别在代码里写if (orderAmount 100) discount 20。所有营销规则满减、折扣、赠品、阶梯价必须定义成可配置的JSON结构由marketing-rule-engine服务解析执行{ activity_id: act_20240601_001, name: 夏日冰饮满减, status: active, scope: [store, online], // 适用范围门店or线上 rules: [ { type: order_amount_threshold, threshold: 100, discount: 20, max_discount: 50 } ], quota: { total: 1000, used: 0, per_user_limit: 1 } }血泪经验per_user_limit字段必须存在且校验逻辑放在rule-engine里而不是前端。曾有项目把“每人限领1张”逻辑放小程序JS里被用户用抓包工具反复提交3分钟刷走200张券。3.2 优惠券发放用Redis原子操作防超发而非MySQL行锁MySQLUPDATE coupon SET used_count used_count 1 WHERE activity_id xxx AND used_count total在高并发下会失效。正确姿势是优惠券库存存Redis HashHINCRBY coupon:act_20240601_001:stock 1若返回值 ≤ 总库存则发放成功否则返回“已发完”成功后异步写MySQL记录明细最终一致性# Python示例redis-py def issue_coupon(activity_id: str, member_id: str) - bool: key fcoupon:{activity_id}:stock # 原子递增返回递增后的新值 current_used redis.hincrby(key, used, 1) total int(redis.hget(key, total)) if current_used total: # 记录发放明细异步任务 save_coupon_issue.delay(activity_id, member_id, current_used) return True else: # 回滚递减计数因为HINCRBY不可逆 redis.hincrby(key, used, -1) return False这里HINCRBY是Redis原生命令毫秒级原子性比MySQL事务快10倍以上。save_coupon_issue.delay用Celery异步落库避免阻塞主流程。3.3 三端状态同步用WebSocket推送而非轮询或Polling小程序/H5/后台管理都需要实时知道“活动还剩几张券”“当前核销率”。轮询浪费带宽Polling延迟高。直接上WebSocket后端用FastAPI WebSockets或Socket.IO建立连接用户进入活动页时前端建立WS连接并传activity_idrule-engine每次发放/核销成功触发publish(activity:act_20240601_001, {used: 127, remain: 873})所有订阅该channel的客户端实时收到更新// 小程序端WebSocket连接uni-app const socket uni.connectSocket({ url: wss://api.yourdomain.com/ws/activity, header: { Authorization: Bearer token } }); socket.onMessage(res { const data JSON.parse(res.data); if (data.channel activity:${this.activityId}) { this.couponRemain data.remain; // 直接更新UI } });避坑WebSocket连接必须带鉴权如JWT且服务端要校验member_id是否有权订阅该活动。曾有项目未校验导致A店员工能监听B店活动数据。4. 后台管理系统的三个致命陷阱权限失控、操作不可逆、数据导出失真后台管理是老板和店长天天盯着的地方但90%的同类系统在这里埋雷店长删了会员数据没人知道财务导出的销售报表和实际到账差2毛区域经理能看到隔壁区的门店数据。这些不是功能缺陷是权限模型和审计机制缺失。4.1 RBAC必须细化到“数据行级”而非仅菜单级“门店管理员”角色不能只控制“能否看到会员列表”而要控制“只能看到本店会员”。常见错误是-- ❌ 错误只校验角色不校验数据归属 SELECT * FROM member WHERE role store_admin; -- ✅ 正确JOIN门店表强制WHERE store_id ? SELECT m.* FROM member m JOIN store s ON m.store_id s.id WHERE s.id ? AND s.manager_id ?;后端API必须在每个查询中注入store_id条件。更安全的做法是所有涉及门店数据的表都加store_id字段并在ORM层全局拦截如SQLAlchemy的before_compile事件。4.2 关键操作必须留“后悔药”且日志可追溯到人“删除会员”“清空优惠券”“重置积分”这类操作不能直接执行DELETE。标准流程是第一步标记为deleted_at NOW()软删除第二步写审计日志包含操作人ID、IP、时间、影响行数、原始SQL参数第三步提供“恢复最近7天软删除数据”按钮仅限超级管理员# 审计日志表结构关键字段 class AuditLog(Base): id Column(String(32), primary_keyTrue) # Snowflake ID operator_id Column(String(32)) # 操作人member_id operator_ip Column(String(45)) # IPv4/IPv6 action Column(String(50)) # delete_member, reset_points target_type Column(String(30)) # member, coupon target_id Column(String(32)) # 被操作对象ID params Column(JSON) # JSON序列化的操作参数 created_at Column(DateTime, defaultfunc.now())提示params字段必须存原始参数如{member_id: m1001, reason: 测试误操作}而不是“操作成功”这种废话。某次生产事故中正是靠这条日志定位到是测试账号误点了清空按钮。4.3 数据导出必须走“离线任务”且文件内容与页面展示完全一致后台点击“导出本月销售报表”如果直接SELECT ... INTO OUTFILE会遇到导出时数据还在变更文件里金额和页面看到的不一致大数据量时请求超时用户以为失败其实后台还在跑CSV中文乱码、Excel公式被破坏正确方案点击导出 → 后端生成任务ID立即返回{task_id: task_20240601_abc}前端轮询/export/status?task_idxxx直到状态为completed下载链接指向Nginx静态文件如/exports/task_20240601_abc.xlsx文件由Celery异步生成# 异步导出任务Celery shared_task def export_sales_report(task_id: str, store_id: str, start_date: str, end_date: str): # 1. 查询时加WITH FOR UPDATE锁定关键数据如订单汇总 # 2. 用openpyxl生成Excel设置单元格格式金额保留2位小数 # 3. 文件存入MinIO生成预签名URL # 4. 更新audit_log表记录导出详情 pass注意Excel必须用openpyxl而非pandas.to_excel后者对样式支持弱且大文件易内存溢出。某次导出10万行订单pandas吃光2GB内存openpyxl稳定在300MB。5. 避坑指南三端联调时最常踩的5个深坑及根治方案联调阶段小程序/H5/后台管理三端互相调用API看似简单实则暗礁密布。以下5条是我在12个同类项目中反复验证过的“必踩坑”每条都附带现象、根因和可落地的解决方案。5.1 现象小程序调用API返回401但H5同样Token却正常原因小程序request默认不携带Cookie而后端Session依赖Cookie校验H5用axios默认withCredentials: true。解决后端API统一改用Token认证HeaderAuthorization: Bearer xxx弃用Cookie Session或小程序端显式开启withCredentials: true需后端CORS配置Access-Control-Allow-Credentials: true且Origin不能为*5.2 现象H5页面在微信内置浏览器打开时部分CSS失效或白屏原因微信iOS版WebView对flex: 1、vh单位兼容性差且禁用部分CSS动画。解决使用postcss-pxtorem将px转rem配合lib-flexible适配vh单位替换为JavaScript动态计算document.documentElement.clientHeight关键动画用transform替代top/left避免触发微信WebView重绘Bug5.3 现象后台管理端修改会员等级后小程序端仍显示旧等级刷新也不变原因小程序端缓存了/member/info接口响应HTTP Cache或Taro/Uniapp的request缓存。解决后端/member/info接口强制加Cache-Control: no-cache, no-store小程序端请求时加时间戳参数/member/info?member_idxxxt${Date.now()}更优方案会员等级变更时通过WebSocket推送{type: member_update, data: {member_id, level}}前端收到后主动刷新5.4 现象优惠券核销后H5页面显示“已使用”但小程序端仍显示“可使用”原因三端各自缓存了优惠券列表且没有统一的缓存失效机制。解决所有优惠券相关接口列表、详情响应头加ETag前端根据ETag做条件请求核销成功后后端调用redis.del(coupon:list:member_id_123)主动失效缓存小程序/H5在核销成功回调里手动清除本地缓存并重新拉取5.5 现象后台导出Excel金额列出现199.99999999999997而非200.00原因JavaScript/Python浮点数精度丢失数据库存的是DECIMAL但中间计算用了float。解决后端所有金额运算用decimal.DecimalPython或BigDecimalJava前端展示金额时用Number(val).toFixed(2)而非val.toFixed(2)避免字符串隐式转换Excel导出时openpyxl设置单元格格式为numbers.FORMAT_NUMBER_006. 一个让老板当场拍板的技巧用“门店热力图”把后台数据变成门店增长引擎后台管理的价值不该止于“查数据”而要成为门店经营的决策中枢。我给3家连锁烘焙品牌落地的方案是把后台的会员消费数据、优惠券核销数据、到店频次数据实时渲染成“门店热力图”并叠加天气、竞品促销、周边地铁客流等外部因子生成可执行的《门店行动建议》。6.1 热力图不是炫技而是解决三个真实问题问题场景传统后台做法热力图驱动方案A店连续两周销售额下滑导出Excel人工对比上周数据热力图显示A店周边3km内新增2家竞品系统自动推送“针对竞品用户定向发5折券”策略B店周末客流暴增但转化率低查“到店人数”和“成交人数”两个孤立数字热力图发现B店周末10-12点排队超20人系统建议“增开1个收银台推预约免排队H5页”C店新会员增长慢看“新增会员数”柱状图热力图结合LBS数据发现C店3km内有3所大学开学系统生成“学生证享首单8折”活动模板6.2 技术实现用GeoHashRedisCanvas零成本接入不用买商业BI工具。核心逻辑只有三步坐标打点用户到店时小程序上报经纬度后端用geohash.encode(lat, lng, precision7)生成wx8g3d这样的短码存Redis SetSADD store:1001:geohash wx8g3d wx8g3e热力聚合每5分钟用GEOPOS批量查出所有门店周边坐标用Canvas绘制热力图颜色深浅点密度策略生成当某区域热力值突增如环比50%触发规则引擎匹配预设策略库# 热力值计算Redis Lua脚本保证原子性 local count redis.call(SCARD, store: .. ARGV[1] .. :geohash) local last_count tonumber(redis.call(GET, store: .. ARGV[1] .. :heat:last)) local ratio count / (last_count 1) if ratio 1.5 then -- 触发策略推送 redis.call(PUBLISH, strategy:trigger, ARGV[1] .. | .. ratio) end redis.call(SET, store: .. ARGV[1] .. :heat:last, count)关键细节precision7的GeoHash精度约100m足够覆盖单个门店辐射范围SCARD比KEYS store:*:geohash快100倍策略推送用Redis Pub/Sub避免轮询。6.3 落地效果从“数据看板”到“增长中台”的最后一公里我们给某社区生鲜品牌上线后店长每天打开后台第一件事不再是看销售额而是看热力图——红色区块代表“高潜力未触达人群”蓝色区块代表“高流失风险老客”。系统自动生成的《行动建议》直接嵌入企业微信店长一键转发给店员执行。三个月后试点门店的会员复购率提升27%优惠券核销率从31%升至68%。老板在季度会上说“这个后台终于不是摆设了。”我坚持在每个新项目启动时花2天时间把热力图框架搭出来。不是为了炫技而是让数据从“老板问我要什么”变成“系统告诉我该做什么”。它逼着团队把会员、营销、门店数据真正打通而不是各管一摊。当你看到店长拿着手机对着热力图说“东门那片红得发亮今天加推试吃装”你就知道这套系统真的活了。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑