1. 项目背景与核心需求
酒水供应商的数字化转型正在加速,传统的电话、Excel表格管理客户订单的方式已经无法满足现代商业需求。一个完整的酒水订购系统需要解决几个核心痛点:
- 客户分散且订单频次高:酒水行业客户包括餐饮店、商超、个人消费者等,订单呈现"小批量、多批次"特点
- 配送时效性要求严格:啤酒、葡萄酒等对储存条件敏感,需要精确的配送时间窗口
- 价格体系复杂:不同客户等级、采购量级对应不同折扣方案
- 库存周转压力大:酒水保质期管理需要与销售数据实时联动
我们设计的这套系统采用SpringBoot+UniApp技术栈,实现了以下核心功能矩阵:
code复制| 模块 | 小程序端功能 | 后台管理功能 |
|-----------------|-------------------------------------|---------------------------------------|
| 客户管理 | 会员注册/登录、资料维护 | 客户分级、消费分析、标签管理 |
| 商品管理 | 商品浏览、分类筛选、详情页 | SKU管理、库存预警、上下架控制 |
| 订单管理 | 购物车、下单、支付、订单追踪 | 订单处理、退换货审核、数据统计 |
| 配送管理 | 配送进度查看、签收确认 | 路线规划、配送员分配、时效监控 |
| 营销中心 | 优惠券领取、促销活动参与 | 活动配置、效果分析、精准推送 |
| 数据分析 | 消费记录查询 | 销售报表、客户画像、库存周转分析 |
2. 技术架构设计
2.1 前后端分离架构
系统采用经典的三层架构:
code复制[UniApp客户端] ←HTTP/HTTPS→ [SpringBoot服务端] ←JDBC→ [MySQL数据库]
↑
↓
[Redis缓存]
前端技术选型考量:
- UniApp的优势:一次开发可发布到微信、支付宝、H5等多端,特别适合酒水行业客户使用场景分散的特点
- Vue语法体系:开发效率高,适合快速迭代的业务需求
- 微信小程序生态:利用微信支付、地理位置等原生能力
后端技术栈关键组件:
- SpringBoot 2.7.x:简化配置,内置Tomcat容器
- MyBatis-Plus:增强的ORM框架,减少样板代码
- Redis:缓存热点数据(如商品详情、促销信息)
- Swagger:API文档自动生成
- Quartz:定时任务(库存同步、订单超时处理)
2.2 数据库设计要点
酒水行业特有的数据模型设计:
sql复制-- 商品表增加酒类特有字段
CREATE TABLE `product` (
`id` bigint NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '商品名称',
`category_id` int NOT NULL COMMENT '酒水分类(1白酒/2啤酒/3葡萄酒...)',
`alcohol_degree` decimal(3,1) DEFAULT NULL COMMENT '酒精度数',
`specification` varchar(50) DEFAULT NULL COMMENT '规格(500ml/箱装等)',
`origin_place` varchar(100) DEFAULT NULL COMMENT '产地',
`shelf_life` int DEFAULT NULL COMMENT '保质期(天)',
`storage_condition` varchar(50) DEFAULT NULL COMMENT '储存条件',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 客户分级定价表
CREATE TABLE `customer_price` (
`id` bigint NOT NULL AUTO_INCREMENT,
`customer_level` tinyint NOT NULL COMMENT '客户等级(1-5)',
`product_id` bigint NOT NULL,
`special_price` decimal(10,2) NOT NULL COMMENT '专享价',
`min_quantity` int DEFAULT '1' COMMENT '起订量',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_level_product` (`customer_level`,`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3. 核心功能实现细节
3.1 小程序端关键技术实现
混合渲染优化方案:
javascript复制// pages/product/detail.vue
export default {
data() {
return {
showWebview: false // 控制复杂图文详情加载时机
}
},
onLoad() {
// 先加载核心数据
this.loadBasicInfo().then(() => {
// 再延迟加载富文本详情
setTimeout(() => {
this.showWebview = true
}, 300)
})
}
}
表单动态校验策略:
javascript复制// 根据客户类型动态设置字段必填规则
const rules = {
companyName: [
{
required: this.form.customerType === 2, // 企业客户必填
message: '请填写公司名称'
}
],
idNumber: [
{
required: this.form.customerType === 1,
validator: (rule, value) => {
return /^\d{17}[\dXx]$/.test(value)
}
}
]
}
3.2 后台管理系统关键接口
订单分页查询接口优化:
java复制@GetMapping("/orders")
public PageResult<OrderVO> queryOrders(
@RequestParam(required = false) String orderNo,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date startDate,
@RequestParam(required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") Date endDate,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "10") Integer size) {
// 构建查询条件
LambdaQueryWrapper<Order> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(StringUtils.isNotBlank(orderNo), Order::getOrderNo, orderNo)
.eq(status != null, Order::getStatus, status)
.ge(startDate != null, Order::getCreateTime, startDate)
.le(endDate != null, Order::getCreateTime, endDate)
.orderByDesc(Order::getCreateTime);
// 使用MyBatis-Plus的分页插件
Page<Order> pageInfo = new Page<>(page, size);
IPage<Order> iPage = orderMapper.selectPage(pageInfo, wrapper);
// 转换VO对象
List<OrderVO> records = iPage.getRecords().stream()
.map(order -> convertToVO(order))
.collect(Collectors.toList());
return new PageResult<>(records, iPage.getTotal());
}
库存扣减的分布式事务处理:
java复制@Transactional
public boolean deductInventory(Long productId, Integer quantity) {
// 1. 检查库存是否充足
Product product = productMapper.selectById(productId);
if (product.getStock() < quantity) {
throw new BusinessException("库存不足");
}
// 2. 扣减库存(乐观锁方案)
int updated = productMapper.updateStock(productId, quantity, product.getVersion());
if (updated == 0) {
throw new ConcurrentUpdateException("库存并发修改冲突");
}
// 3. 记录库存变更流水
InventoryLog log = new InventoryLog();
log.setProductId(productId);
log.setChangeAmount(-quantity);
log.setRemarks("订单扣减");
inventoryLogMapper.insert(log);
return true;
}
4. 配送系统实现方案
4.1 配送路线智能规划
基于百度地图API实现的多点路径优化算法:
javascript复制// 使用百度地图JavaScript API的路径规划服务
function optimizeDeliveryRoute(deliveryPoints) {
const map = new BMap.Map();
const driving = new BMap.DrivingRoute(map, {
policy: BMAP_DRIVING_POLICY_LEAST_TIME,
onSearchComplete: function(results) {
if (driving.getStatus() == BMAP_STATUS_SUCCESS) {
const plan = results.getPlan(0);
const optimizedRoute = plan.getRoute(0).getPath();
// 返回优化后的路径点坐标数组
return optimizedRoute;
}
}
});
driving.search(deliveryPoints);
}
4.2 实时位置追踪实现
小程序端位置上报逻辑:
javascript复制// utils/location.js
let locationTimer = null;
export function startTrackDelivery(deliveryId) {
// 每30秒上报一次位置
locationTimer = setInterval(() => {
uni.getLocation({
type: 'gcj02',
success: (res) => {
uni.request({
url: '/api/delivery/location',
method: 'POST',
data: {
deliveryId,
latitude: res.latitude,
longitude: res.longitude,
timestamp: Date.now()
}
});
}
});
}, 30000);
}
export function stopTrackDelivery() {
if (locationTimer) {
clearInterval(locationTimer);
}
}
后台位置处理服务:
java复制// 使用WebSocket推送实时位置
@ServerEndpoint("/tracking/{deliveryId}")
@Component
public class DeliveryTrackingEndpoint {
private static ConcurrentHashMap<Long, Session> sessions = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("deliveryId") Long deliveryId) {
sessions.put(deliveryId, session);
}
public static void sendLocationUpdate(DeliveryLocation location) {
Session session = sessions.get(location.getDeliveryId());
if (session != null && session.isOpen()) {
session.getAsyncRemote().sendText(JSON.toJSONString(location));
}
}
}
5. 项目部署与运维
5.1 小程序发布注意事项
-
域名配置白名单:
- 登录微信公众平台→开发→开发设置→服务器域名
- 添加request合法域名(API服务地址)
- 添加uploadFile合法域名(文件上传地址)
- 添加downloadFile合法域名(文件下载地址)
-
隐私协议合规:
xml复制<!-- 在app.json中配置隐私协议 --> { "__usePrivacyCheck__": true, "plugins": {}, "permission": { "scope.userLocation": { "desc": "用于配送位置追踪" } } }
5.2 服务端部署方案
推荐使用Docker Compose部署:
yaml复制version: '3'
services:
app:
image: openjdk:11-jre
ports:
- "8080:8080"
volumes:
- ./app.jar:/app.jar
- ./logs:/logs
command: java -jar /app.jar
depends_on:
- redis
- mysql
mysql:
image: mysql:5.7
environment:
MYSQL_ROOT_PASSWORD: yourpassword
MYSQL_DATABASE: liquor_db
ports:
- "3306:3306"
volumes:
- ./mysql-data:/var/lib/mysql
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
- ./redis-data:/data
5.3 性能优化实战经验
-
小程序包体积控制:
- 使用uni-app的"分包加载"功能
javascript复制// pages.json { "subPackages": [ { "root": "packageA", "pages": [ {"path": "page1", "style": {}}, {"path": "page2", "style": {}} ] } ] } -
API响应缓存策略:
java复制@Cacheable(value = "products", key = "#id") public Product getProductById(Long id) { return productMapper.selectById(id); } @CacheEvict(value = "products", key = "#product.id") public void updateProduct(Product product) { productMapper.updateById(product); } -
数据库查询优化案例:
sql复制-- 优化前的慢查询 SELECT * FROM orders WHERE status = 1 ORDER BY create_time DESC; -- 优化方案:添加复合索引 ALTER TABLE orders ADD INDEX idx_status_createtime (status, create_time DESC); -- 分页优化 SELECT * FROM orders WHERE status = 1 AND id < last_id ORDER BY id DESC LIMIT 10;
6. 典型问题排查实录
6.1 微信登录失败问题排查
现象:部分安卓设备无法完成微信登录
排查过程:
-
检查微信开放平台应用配置:
- 包名:com.example.liquor(必须与uniapp打包配置完全一致)
- 签名:使用微信提供的签名工具获取正确的MD5值
-
发现uniapp打包时的默认签名与微信注册签名不一致:
shell复制# 生成签名命令 keytool -list -v -keystore my-release-key.keystore -
解决方案:
- 在微信开放平台注册签名时使用uniapp默认证书
- 或打包时指定与微信注册一致的keystore
6.2 订单超时未关闭问题
现象:30分钟未支付的订单未自动关闭
排查步骤:
-
检查Quartz任务配置:
java复制@Bean public JobDetail orderTimeoutJob() { return JobBuilder.newJob(OrderTimeoutJob.class) .withIdentity("orderTimeoutJob") .storeDurably() .build(); } @Bean public Trigger orderTimeoutTrigger() { return TriggerBuilder.newTrigger() .forJob(orderTimeoutJob()) .withIdentity("orderTimeoutTrigger") .withSchedule(CronScheduleBuilder.cronSchedule("0 */1 * * * ?")) // 每分钟执行 .build(); } -
发现服务器时区设置为UTC,与业务时区不一致:
java复制// 修复方案:在启动类设置默认时区 @PostConstruct void started() { TimeZone.setDefault(TimeZone.getTimeZone("Asia/Shanghai")); } -
增加任务执行日志监控:
java复制public class OrderTimeoutJob implements Job { private static final Logger logger = LoggerFactory.getLogger(OrderTimeoutJob.class); @Override public void execute(JobExecutionContext context) { logger.info("开始执行订单超时检查..."); // 业务逻辑 } }
7. 安全防护方案
7.1 接口防刷策略
-
图形验证码:
java复制@GetMapping("/captcha") public void generateCaptcha(HttpServletResponse response) throws IOException { // 生成4位随机数 String code = RandomStringUtils.randomNumeric(4); // 存入Redis,有效期5分钟 redisTemplate.opsForValue().set("CAPTCHA:"+code, code, 5, TimeUnit.MINUTES); // 生成图片 BufferedImage image = new CaptchaGenerator().generate(code); response.setContentType("image/jpeg"); ImageIO.write(image, "jpg", response.getOutputStream()); } -
IP限流控制:
java复制@Aspect @Component public class RateLimitAspect { private final RedisTemplate<String, Object> redisTemplate; @Around("@annotation(rateLimit)") public Object around(ProceedingJoinPoint joinPoint, RateLimit rateLimit) throws Throwable { String key = "RATE_LIMIT:" + getClientIP(); Long count = redisTemplate.opsForValue().increment(key, 1); if (count == 1) { redisTemplate.expire(key, rateLimit.time(), rateLimit.unit()); } if (count > rateLimit.count()) { throw new BusinessException("操作过于频繁,请稍后再试"); } return joinPoint.proceed(); } }
7.2 敏感数据保护
-
数据库字段加密:
java复制@ColumnTransformer( read = "AES_DECRYPT(UNHEX(phone), 'encryption_key')", write = "HEX(AES_ENCRYPT(?, 'encryption_key'))" ) @Column(name = "phone") private String phone; -
接口数据脱敏:
java复制public static String maskMobile(String mobile) { if (StringUtils.isBlank(mobile) || mobile.length() < 7) { return mobile; } return mobile.substring(0, 3) + "****" + mobile.substring(7); }
8. 项目扩展方向
8.1 智能推荐模块
基于用户行为的协同过滤算法实现:
python复制# 使用Surprise库实现推荐算法
from surprise import Dataset, KNNBasic
from surprise.model_selection import train_test_split
def train_recommend_model():
# 加载用户-商品评分数据
data = Dataset.load_from_df(ratings_df[['user_id', 'product_id', 'rating']],
reader=Reader(rating_scale=(1, 5)))
trainset, testset = train_test_split(data, test_size=0.25)
# 使用KNN基础算法
algo = KNNBasic()
algo.fit(trainset)
# 保存模型
dump.dump('recommend_model', algo=algo)
return algo
8.2 供应商协同平台
区块链技术在供应链金融中的应用设计:
solidity复制// 简单的智能合约示例 - 订单融资
pragma solidity ^0.8.0;
contract OrderFinance {
struct Order {
address supplier;
address purchaser;
uint amount;
uint dueDate;
bool isFinanced;
}
mapping(uint => Order) public orders;
uint public orderCount;
event OrderCreated(uint orderId);
event OrderFinanced(uint orderId, address financier);
function createOrder(address purchaser, uint amount, uint dueDate) external {
orderCount++;
orders[orderCount] = Order(msg.sender, purchaser, amount, dueDate, false);
emit OrderCreated(orderCount);
}
function financeOrder(uint orderId) external payable {
require(!orders[orderId].isFinanced, "Already financed");
require(msg.value >= orders[orderId].amount, "Insufficient funds");
orders[orderId].isFinanced = true;
payable(orders[orderId].supplier).transfer(msg.value);
emit OrderFinanced(orderId, msg.sender);
}
}
在实际开发中,我们遇到的最棘手问题是UniApp在多端适配时的样式兼容性问题。特别是酒水商品的详情页,在iOS和Android设备上显示效果差异较大。最终我们采用了以下解决方案:
- 建立多端样式兼容层:
css复制/* 通用样式 */
.product-title {
font-size: 16px;
/* #ifdef MP-WEIXIN */
padding: 10rpx;
/* #endif */
/* #ifdef H5 */
padding: 8px;
/* #endif */
}
- 使用条件编译处理平台差异:
javascript复制// 获取导航栏高度
function getNavBarHeight() {
// #ifdef MP-WEIXIN
const systemInfo = uni.getSystemInfoSync()
const menuButtonInfo = uni.getMenuButtonBoundingClientRect()
return menuButtonInfo.bottom + 8
// #endif
// #ifdef H5
return 44
// #endif
}
