汉服租赁管理系统是一个基于SpringBoot框架开发的B/S架构应用,旨在为汉服租赁商家提供一套完整的数字化管理解决方案。这个系统从实际业务需求出发,解决了传统汉服租赁行业中的几个核心痛点:
我在实际开发过程中发现,一个合格的租赁系统需要特别关注以下几个技术要点:
后端技术栈:
前端技术栈:
数据库:
技术选型心得:在初期技术调研时,我对比了JPA和MyBatis-Plus,最终选择后者是因为租赁业务中有大量复杂查询场景,MyBatis-Plus的Wrapper查询构建器能更灵活地应对这些需求。
code复制┌───────────────────────────────────────────────────────┐
│ 客户端浏览器 │
└───────────────┬───────────────────────┬───────────────┘
│ │
┌───────────────▼───────┐ ┌───────────▼───────────────┐
│ Vue3前端应用 │ │ 移动端H5 │
└───────────────┬───────┘ └───────────┬───────────────┘
│ │
└───────────┬───────────┘
│
┌───────────▼───────────┐
│ API网关层 │
│ (Spring Cloud Gateway)│
└───────────┬───────────┘
│
┌───────────▼───────────┐
│ 业务微服务 │
│ │
│ ┌─────────────────┐ │
│ │ 用户服务 │ │
│ └─────────────────┘ │
│ ┌─────────────────┐ │
│ │ 订单服务 │ │
│ └─────────────────┘ │
│ ┌─────────────────┐ │
│ │ 库存服务 │ │
│ └─────────────────┘ │
│ ┌─────────────────┐ │
│ │ 支付服务 │ │
│ └─────────────────┘ │
└───────────┬───────────┘
│
┌───────────▼───────────┐
│ 数据存储层 │
│ │
│ ┌─────────────────┐ │
│ │ MySQL │ │
│ └─────────────────┘ │
│ ┌─────────────────┐ │
│ │ Redis │ │
│ └─────────────────┘ │
│ ┌─────────────────┐ │
│ │ MongoDB │ │
│ └─────────────────┘ │
└───────────────────────┘
核心表结构设计:
sql复制CREATE TABLE `costume` (
`id` bigint NOT NULL AUTO_INCREMENT,
`code` varchar(32) NOT NULL COMMENT '服装编号',
`name` varchar(100) NOT NULL COMMENT '服装名称',
`style_id` int NOT NULL COMMENT '款式ID',
`size` varchar(10) NOT NULL COMMENT '尺码',
`color` varchar(20) NOT NULL COMMENT '颜色',
`material` varchar(50) DEFAULT NULL COMMENT '材质',
`price` decimal(10,2) NOT NULL COMMENT '日租金',
`deposit` decimal(10,2) NOT NULL COMMENT '押金',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '状态(0-待租,1-租赁中,2-清洗中,3-维修中)',
`location` varchar(100) DEFAULT NULL COMMENT '存放位置',
`images` text COMMENT '图片JSON数组',
`description` text COMMENT '详细描述',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_code` (`code`),
KEY `idx_style` (`style_id`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='汉服信息表';
sql复制CREATE TABLE `rent_order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`order_no` varchar(32) NOT NULL COMMENT '订单编号',
`user_id` bigint NOT NULL COMMENT '用户ID',
`total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额',
`pay_amount` decimal(10,2) NOT NULL COMMENT '实付金额',
`deposit_amount` decimal(10,2) NOT NULL COMMENT '押金金额',
`status` tinyint NOT NULL DEFAULT '0' COMMENT '订单状态(0-待支付,1-已支付,2-已取消,3-已完成)',
`rent_start` date NOT NULL COMMENT '租赁开始日期',
`rent_end` date NOT NULL COMMENT '租赁结束日期',
`contact_name` varchar(50) NOT NULL COMMENT '联系人',
`contact_phone` varchar(20) NOT NULL COMMENT '联系电话',
`address` varchar(200) DEFAULT NULL COMMENT '配送地址',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_order_no` (`order_no`),
KEY `idx_user` (`user_id`),
KEY `idx_status` (`status`),
KEY `idx_rent_date` (`rent_start`,`rent_end`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租赁订单表';
数据库设计心得:在汉服租赁场景中,日期区间查询非常频繁(如查询某时间段内可租的服装),因此需要特别注意日期相关字段的索引设计。同时,服装状态变更是一个高频操作,status字段也需要单独建立索引。
核心租赁流程:
关键代码实现 - 库存检查:
java复制@Transactional
public OrderDTO createOrder(OrderCreateVO createVO) {
// 1. 参数校验
validateOrderParams(createVO);
// 2. 检查服装可用性(分布式锁防止超卖)
String lockKey = "inventory_lock:" + createVO.getCostumeId();
try {
boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("当前服装正在被其他用户预订,请稍后再试");
}
Costume costume = costumeMapper.selectById(createVO.getCostumeId());
if (costume == null || costume.getStatus() != CostumeStatus.AVAILABLE.getCode()) {
throw new BusinessException("所选服装当前不可租");
}
// 3. 检查日期冲突
int conflictCount = orderMapper.countDateConflict(
createVO.getCostumeId(),
createVO.getRentStart(),
createVO.getRentEnd());
if (conflictCount > 0) {
throw new BusinessException("所选时间段该服装已被预订");
}
// 4. 创建订单
Order order = new Order();
BeanUtils.copyProperties(createVO, order);
order.setOrderNo(generateOrderNo());
order.setStatus(OrderStatus.UNPAID.getCode());
orderMapper.insert(order);
// 5. 更新服装状态
costume.setStatus(CostumeStatus.RESERVED.getCode());
costumeMapper.updateById(costume);
return convertToDTO(order);
} finally {
redisTemplate.delete(lockKey);
}
}
采用RBAC(基于角色的访问控制)模型设计:
java复制public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
factoryBean.setSecurityManager(securityManager);
// 权限配置
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/admin/**", "roles[admin]");
filterMap.put("/order/**", "authc,perms[order:manage]");
filterMap.put("/costume/**", "authc,perms[costume:manage]");
filterMap.put("/user/**", "authc,perms[user:manage]");
filterMap.put("/**", "authc");
factoryBean.setFilterChainDefinitionMap(filterMap);
return factoryBean;
}
}
权限设计要点:
系统集成了两种支付方式:
支付状态回调处理:
java复制@RestController
@RequestMapping("/api/payment")
public class PaymentController {
@PostMapping("/alipay/callback")
public String alipayCallback(HttpServletRequest request) {
Map<String, String> params = convertRequestToMap(request);
try {
// 验证签名
boolean signVerified = AlipaySignature.rsaCheckV1(
params,
alipayConfig.getAlipayPublicKey(),
alipayConfig.getCharset(),
alipayConfig.getSignType());
if (!signVerified) {
return "failure";
}
String tradeStatus = params.get("trade_status");
String orderNo = params.get("out_trade_no");
if ("TRADE_SUCCESS".equals(tradeStatus)) {
orderService.handlePaymentSuccess(orderNo);
}
return "success";
} catch (Exception e) {
log.error("支付宝回调处理异常", e);
return "failure";
}
}
}
基于用户历史租赁记录和服装标签,实现简单的推荐算法:
java复制public List<Costume> recommendCostumes(Long userId) {
// 1. 获取用户历史租赁记录
List<Order> userOrders = orderMapper.selectByUser(userId);
// 2. 提取用户偏好的风格标签
Set<Integer> preferredStyles = userOrders.stream()
.map(order -> orderMapper.selectCostumeStyle(order.getCostumeId()))
.filter(Objects::nonNull)
.collect(Collectors.toSet());
// 3. 如果无历史记录,返回热门租赁
if (preferredStyles.isEmpty()) {
return costumeMapper.selectHotCostumes(10);
}
// 4. 基于偏好推荐
return costumeMapper.selectByStyles(
new ArrayList<>(preferredStyles),
CostumeStatus.AVAILABLE.getCode(),
10);
}
前端实现可视化租赁日历,直观展示服装的可用状态:
vue复制<template>
<div class="calendar-container">
<div class="calendar-header">
<button @click="prevMonth">上个月</button>
<h2>{{ currentYear }}年{{ currentMonth }}月</h2>
<button @click="nextMonth">下个月</button>
</div>
<div class="calendar-grid">
<div v-for="day in days" :key="day.date"
:class="['calendar-day', getDayClass(day)]"
@click="selectDay(day)">
<div class="day-number">{{ day.date.getDate() }}</div>
<div class="day-status">{{ getStatusText(day) }}</div>
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
currentYear: new Date().getFullYear(),
currentMonth: new Date().getMonth() + 1,
bookedDates: []
}
},
computed: {
days() {
// 生成当月日历数据
const days = [];
const daysInMonth = new Date(this.currentYear, this.currentMonth, 0).getDate();
for (let i = 1; i <= daysInMonth; i++) {
const date = new Date(this.currentYear, this.currentMonth - 1, i);
const isBooked = this.bookedDates.some(d =>
d.getFullYear() === date.getFullYear() &&
d.getMonth() === date.getMonth() &&
d.getDate() === date.getDate()
);
days.push({
date,
isBooked,
isToday: this.isSameDay(date, new Date())
});
}
return days;
}
},
methods: {
getDayClass(day) {
return {
'booked': day.isBooked,
'today': day.isToday,
'weekend': [0, 6].includes(day.date.getDay())
};
},
selectDay(day) {
if (!day.isBooked) {
this.$emit('day-selected', day.date);
}
}
}
}
</script>
推荐配置:
部署步骤:
bash复制mysql -u root -p < schema.sql
mysql -u root -p < data.sql
bash复制nohup java -jar hanfu-rental.jar --spring.profiles.active=prod > app.log 2>&1 &
bash复制npm run build
cp -r dist/* /usr/local/nginx/html/
数据库优化:
缓存策略:
JVM调优:
bash复制java -jar -Xms512m -Xmx1024m -XX:+UseG1GC hanfu-rental.jar
问题现象:
在高并发场景下,同一件汉服可能被多个用户同时预订。
解决方案:
java复制@Update("update costume set status=#{newStatus}, version=version+1 " +
"where id=#{id} and version=#{version}")
int updateWithVersion(Costume costume);
问题场景:
用户A和用户B几乎同时预订同一服装的相同日期。
解决方案:
sql复制ALTER TABLE `order_detail`
ADD UNIQUE KEY `uk_costume_date` (`costume_id`, `rent_date`);
java复制@Transactional
public void createOrder() {
// 检查冲突
// 创建订单
// 更新库存
}
问题:
汉服图片较多,直接存储在数据库影响性能。
解决方案:
这个汉服租赁管理系统从设计到实现历时3个月,期间遇到了不少挑战,特别是在高并发场景下的数据一致性问题。通过这个项目,我深刻理解了分布式锁的应用场景和实现原理。系统目前已经稳定运行了半年,日均处理订单100+,未来还会继续迭代优化。