1. 项目概述
"升鲜宝"是一款面向生鲜零售行业的SaaS收银系统,采用"每租户独立数据库+主库"的混合架构模式。这套系统专门为连锁生鲜门店设计,解决多门店统一管理和独立运营的矛盾需求。我在实际部署中发现,生鲜行业对数据隔离和实时性有着特殊要求——不同门店的商品价格、促销策略需要独立配置,但总部又需要实时汇总销售数据。这种业务特性使得传统的单数据库架构难以满足需求。
系统基于Spring Boot框架开发,部署在阿里云RDS+ECS环境,通过Nginx实现负载均衡。核心功能包括租户生命周期管理、套餐定价、自动计费、模块授权等,特别针对生鲜行业设计了库存预警、批次追踪等特色功能模块。数据库设计采用Oracle兼容方案,既满足企业级事务需求,又保留了向其他数据库迁移的可能性。
2. 核心需求解析
2.1 生鲜行业特殊需求
生鲜门店的运营有三个显著特点:一是商品保质期短导致价格变动频繁,二是需要严格的批次追溯,三是早晚市流量波动大。这就要求收银系统必须:
- 支持动态价格策略(如临期商品自动折扣)
- 完整的进销存追溯能力
- 高并发下的稳定结算
我们在tenant表中特别设计了status字段(1正常 2试用 3冻结 4欠费停用),就是为了应对生鲜门店可能突然停业、临时续费等场景。比如当门店因装修暂停营业时,可以立即冻结系统访问而不丢失历史数据。
2.2 多租户架构选择
常见的SaaS多租户方案有:
- 共享表(通过tenant_id区分)
- 共享数据库独立schema
- 完全独立数据库
经过压力测试,最终选择"独立数据库+主库"方案是因为:
- 生鲜交易数据量大(单店日均2000+订单)
- 各门店业务完全独立
- 符合连锁企业数据主权要求
主库(saas_master_db)仅存放租户元数据和跨店汇总信息,各门店业务数据存储在独立的租户库中。这种设计在阿里云RDS上实现成本可控,单个租户库月费用约150元。
3. 数据库详细设计
3.1 租户域核心表
tenant表关键设计点:
CREATE TABLE tenant ( id BIGINT PRIMARY KEY COMMENT '租户ID', tenant_code VARCHAR(64) NOT NULL COMMENT '租户编码', tenant_name VARCHAR(128) NOT NULL COMMENT '租户名称', status INT NOT NULL DEFAULT 1 COMMENT '1正常 2试用 3冻结 4欠费停用', trial_end_time BIGINT COMMENT '试用结束时间', current_plan_id BIGINT COMMENT '当前套餐ID', expire_time BIGINT COMMENT '到期时间' ) ENGINE=InnoDB;特别说明几个字段的业务含义:
- tenant_code采用"区域+门店编号"规则(如BJ-001)
- status=3(冻结)时仍可查询历史数据但不能新增交易
- expire_time精确到分钟级,避免生鲜早市开始时出现服务中断
tenant_admin表安全设计:
CREATE TABLE tenant_admin ( password_hash VARCHAR(255) NOT NULL COMMENT 'PBKDF2WithHmacSHA256加密', phone VARCHAR(32) COMMENT '短信验证绑定', enabled TINYINT DEFAULT 1 COMMENT '0表示禁用' );采用PBKDF2算法加密密码,配合短信二次验证。实测显示,这种方案在保证安全性的同时,登录耗时控制在800ms内,不影响收银效率。
3.2 套餐与定价模型
生鲜行业的套餐设计需要特别考虑:
- 基础功能(收银、库存)
- 增值模块(配送、会员营销)
- 用量计费(API调用、打印次数)
saas_plan表关键字段:
billing_cycle INT NOT NULL COMMENT '1月付 2年付', base_price DECIMAL(18,2) NOT NULL COMMENT '基础价格', trial_days INT DEFAULT 0 COMMENT '试用天数'典型套餐配置示例:
- 基础版:包含POS+库存,月付299元
- 专业版:增加批次追溯,年付2999元(省25%)
价格组件表(saas_price_component)支持三种计费方式:
- 订阅式:固定周期收费
- 用量式:按实际使用量
- 一次性:如开通费
3.3 订阅与计费流程
生鲜行业特有的账单周期问题:
- 多数门店选择自然月结算
- 但新店开业常在月中,需要按比例计费
tenant_subscription表处理逻辑:
CREATE TABLE tenant_subscription ( start_time BIGINT NOT NULL COMMENT '精确到毫秒', end_time BIGINT NOT NULL, billing_cycle INT NOT NULL COMMENT '1月付 2年付', auto_renew TINYINT DEFAULT 0 );续费流程特别处理:
- 提前7天扫描到期租户
- 生成invoice时计算闰月等情况
- 支持"冻结期"(欠费后3天内可恢复)
4. 关键业务实现
4.1 自动续费任务
采用Spring Scheduler实现每日凌晨2点执行:
@Scheduled(cron = "0 0 2 * * ?") public void autoRenewJob() { // 查询7天内到期的租户 List<Tenant> expiringTenants = tenantMapper.selectExpiring(); expiringTenants.forEach(tenant -> { // 生成发票 Invoice invoice = billingService.generateInvoice(tenant); if (tenant.getAutoRenew() == 1) { // 自动扣款 paymentService.autoCharge(tenant, invoice); } }); }实际运营中发现,生鲜门店更喜欢上午续费,因此将执行时间调整为早6点,成功率提升18%。
4.2 配额控制实现
tenant_entitlement_snapshot表的quota_json存储格式:
{ "POS_TERMINAL": 2, "REPORT_EXPORT": 50, "INVENTORY_ALERT": true }在Spring Boot中通过AOP实现配额校验:
@Around("@annotation(quotaCheck)") public Object checkQuota(ProceedingJoinPoint joinPoint) { String feature = quotaCheck.value(); int currentUsage = getCurrentUsage(feature); int limit = getQuotaLimit(feature); if (currentUsage >= limit) { throw new QuotaExceededException(); } return joinPoint.proceed(); }5. 性能优化实践
5.1 数据库分片策略
租户数据库按地区分片:
- 华北区域:tenant_db_hb_001 ~ 100
- 华东区域:tenant_db_hd_001 ~ 100
通过ShardingSphere实现路由:
spring: shardingsphere: datasource: names: ds-master,ds-hb-001,ds-hd-001 sharding: tables: tenant: actual-data-nodes: ds-master.tenant order: actual-data-nodes: ds-hb-$->{0..99}.order,ds-hd-$->{0..99}.order database-strategy: standard: precise-algorithm-class-name: com.xfresh.db.region.PreciseShardingAlgorithm range-algorithm-class-name: com.xfresh.db.region.RangeShardingAlgorithm5.2 缓存设计
采用多级缓存策略:
- 本地缓存(Caffeine):存储租户基础信息,TTL=5分钟
- Redis集群:存储套餐详情、配额数据,TTL=1小时
- 数据库:最终数据源
缓存更新策略:
- 租户信息变更时主动失效缓存
- 套餐数据采用定时刷新
6. 踩坑与解决方案
6.1 时间戳问题
初期使用LocalDateTime存储时间,在跨时区部署时出现计费错误。改为统一存储UTC时间戳:
expire_time BIGINT COMMENT 'UTC毫秒时间戳'在Java端处理:
public long calculateExpireTime(int billingCycle) { Instant now = Instant.now(); if (billingCycle == MONTHLY) { return now.plus(30, ChronoUnit.DAYS).toEpochMilli(); } else { return now.plus(365, ChronoUnit.DAYS).toEpochMilli(); } }6.2 账单精度问题
生鲜行业常有0.99元等定价,初期使用float导致金额误差。全部改为DECIMAL(18,2):
total_amount DECIMAL(18,2) NOT NULL COMMENT '精确到分'在Java中使用BigDecimal进行计算:
BigDecimal price = new BigDecimal("9.99"); BigDecimal quantity = new BigDecimal("3"); BigDecimal total = price.multiply(quantity); // 29.977. 企业级扩展
7.1 生鲜特色模块
在saas_plan_module中增加行业专用模块:
- 批次追溯(TRACE)
- 临期预警(EXPIRE_ALERT)
- 产地溯源(ORIGIN_TRACE)
配置示例:
INSERT INTO saas_plan_module VALUES (1, 101, 'TRACE', 1), (2, 101, 'EXPIRE_ALERT', 1);7.2 硬件集成方案
通过quota_json控制外设数量:
{ "SCALE": 2, "PRINTER": 1, "CARD_READER": 1 }实际部署中发现,生鲜门店常用设备包括:
- 电子秤(通过串口通信)
- 小票打印机(ESC/POS指令)
- 扫码枪(HID设备)
8. 监控与运维
8.1 关键指标监控
- 租户数据库空间使用率(生鲜数据增长快)
- 日结账单生成耗时(晚高峰时段)
- 电子秤通信延迟(影响称重效率)
通过Prometheus+Granfa实现看板:
- job_name: 'xfresh_saas' metrics_path: '/actuator/prometheus' static_configs: - targets: ['10.0.0.1:8080']8.2 灾备方案
针对生鲜行业早高峰特点设计:
- 每日4:00进行数据库快照
- 交易数据实时同步到备库
- 出现故障时5分钟内切换备用POS机
备份策略示例:
# 每天凌晨全量备份 0 4 * * * pg_dump -U postgres -d tenant_db_001 > /backup/tenant_001_$(date +\%Y\%m\%d).sql