ARTICLE · 1358200
JAVA露营租赁露营装备租赁系统源码支持小程序
CAMP RENTAL SYSTEM / JAVA 露营装备租赁 · 小程序 · 可二开源码 JAVA露营装备租赁系统源码支持小程序支持小程序的一体化装备租赁、商家协作与押金管理实现 Spring Boot · MyBatis-Plus · MySQLuni-app · Vue + ElementUI 装备租赁 / 分类管理 / 地址管理商家中心 / 我的团队 / 押金管理 |
露营装备租赁的难点,通常不在“把装备摆上货架”。真正消耗运营精力的是三件事:同一顶帐篷在周末被重复下单、押金收退缺少可追溯凭证、门店与团队之间的权限和结算说不清。
这套系统围绕装备、订单、押金三条主线展开:用户在小程序里按日期选装备、看押金、下单;商家在后台审核装备、维护库存、履约订单;平台用统一账本记录每一笔押金冻结与退还。
下面按一笔租赁订单的真实链路拆解六个功能模块,再给出三端分工、数据库表结构与核心代码。文中截图均为演示数据,已对手机号、门店地址和订单编号做脱敏处理。
一、功能总览
六个功能点不是并列的菜单,而是同一笔订单在不同阶段的能力。先用一张表把“它解决什么问题”说清楚,后面再沿着业务链路展开。
01 / RENTAL 露营装备租赁 解决同一件装备被重复下单、租金与押金口径不一的问题。 | 02 / CATEGORY 分类管理 解决装备越上越多、用户找不到货,运营改类目还要发版的问题。 |
03 / ADDRESS 地址管理 解决门店自提与同城送达共用一套履约信息、地址散落的问题。 | 04 / MERCHANT 商家中心 解决多商家各自维护一套表、平台审核与数据边界不清的问题。 |
05 / TEAM 我的团队 解决多人共用账号、门店权限和操作记录说不清的问题。 | 06 / DEPOSIT 押金管理 解决押金口头约定、冻结与退还没有流水可查的问题。 |
二、功能拆解
一笔订单的链路可以压缩成:分类筛选 → 装备详情 → 选租期 → 提交订单并冻结押金 → 门店自提/同城送达 → 归还验机 → 押金结算。下面六个模块分别落在这条链路的不同位置,每个模块都按“输入—状态—输出”说明。
01 露营装备租赁:把“按天租”算清楚
装备租赁模块不是商品列表的变体。它要同时回答四个问题:这件装备在所选日期是否可租、租金按几天算、押金按什么标准冻结、归还后库存何时回到可租池。
系统把日租金、押金、可租数量拆成独立字段。下单时服务端重新读取装备状态和库存,不采用前端传来的价格;订单写入装备标题、日租金、押金快照,后续商家改价不会影响历史订单。
库存不是简单的减一。下单先占用可租库存并计入锁定库存,取货后从锁定转入在租,归还验机后再回到可租。未支付订单超时关闭时,在同一事务里释放库存并解冻押金,避免“订单关了、库存还锁着”。
前端负责展示可租日期、价格明细和状态文案;可租判断、金额计算和状态流转都放在服务端。前端禁用按钮只能减少误触,不能替代后端的并发控制。
![]() 【JAVA露营租赁露营装备租赁系统源码支持小程序】实拍 01|首页推荐、分类筛选与租物车,对应装备发现、分类浏览与下单前确认;截图数据为演示数据。 |
02 分类管理:让运营改类目不用发版
分类是运营工具,不是前端写死的 tab。数据库用 parent_id 维护树形结构,分类的图标、排序、启停由后台配置;装备通过 category_id 绑定分类。这样新增“天幕”“炊具”“照明”时,不需要改代码重新发版。
实现上先校验父子关系,禁止把父级移动到自己的子节点;有装备绑定的分类不物理删除,只允许停用。分类树可以缓存,但装备新增或审核通过后要主动失效缓存,避免用户端长时间看到旧结构。
对多商家场景,可以把分类做成“平台模板 + 商家自定义排序”:平台保证类目口径一致,商家只调整自己店铺的展示顺序。历史订单保存分类名称快照,分类改名不会影响已经发生的订单。
03 地址管理:自提与送达的履约信息
地址管理包含两类数据:用户的收货/联系人地址,以及商家的门店自提地址。两者不要混在一张表里,否则权限和脱敏边界会变得模糊。
默认地址切换要在事务里先把该用户其他地址置为非默认,再设置当前地址;地址软删除,历史订单保留地址快照。手机号在库里用 AES-GCM 加密,展示时只返回 138****0000;商家只能在履约订单中看到必要的联系人信息,导出和日志同样脱敏。
默认地址切换、地址删除都要校验 user_id,不能只凭 address_id。门店自提还要校验门店状态和营业时间;同城送达则按门店配送范围计算,超出范围时在提交订单前就给出提示,而不是支付后再取消。
![]() 【JAVA露营租赁露营装备租赁系统源码支持小程序】实拍 02|个人中心、装备详情与确认订单,展示地址管理入口、门店自提与联系人填写;截图数据为演示数据。 |
04 商家中心:先把多商家数据边界立住
商家中心解决的是供给侧的秩序:谁能上架、上架什么、以什么押金规则出租、订单由谁履约。它不是一个“商家也能登录的后台”,而是带数据隔离的多租户工作台。
多商家系统最重要的是数据边界。merchant_id 不能由前端随意传,服务端要从登录态和团队成员关系推导;商家只能看自己店铺的订单、库存和结算。平台管理员才拥有跨商家查询权限。
装备上架前必须审核,押金规则要在用户下单前展示;订单支付后,商家不能直接改租金和押金,只能发起售后或备注。所有审核、改价、退款操作写入操作日志,保留操作人、时间、前后值。
05 我的团队:角色、门店与操作留痕
我的团队不是社交裂变工具,而是门店协作的权限模型。建议至少区分 OWNER(负责人)、MANAGER(店长)、STAFF(店员)、CHECKER(核销/验机)四类角色。
菜单隐藏只是展示层,接口必须用方法级权限和数据范围校验;团队邀请要校验邀请人权限,不能通过改 URL 把别人拉进商家。成员停用后,未完成的订单要转交给有权限的负责人,避免履约断档。
如果做佣金,只对已完成租赁订单计算,规则提前公示;不要收入门费、拉人头返利或多层级分佣。这类模式容易触碰传销和平台规则红线,也会把一套租赁系统带偏。
06 押金管理:冻结、扣减与退还流水
押金管理是这套系统最需要克制的地方:押金不是平台的收入,而是一笔有条件的负债。用户下单时冻结,归还验机后解冻;如果发生损坏,先由商家提交证据和扣减原因,平台审核后再记一笔扣减流水,剩余部分退回。
退款要原路退回,退款接口用biz_no + type 唯一约束做幂等,避免重复退款。用户端展示的“可提现余额”和“冻结押金”必须分开,不能用同一个余额字段混算;否则一旦退款失败,账就对不上。
对账时至少核对三组数:支付渠道的收款/退款流水、订单的租金与押金、钱包与押金流水的余额。三组数能对上,才说明这笔订单真正结清。
![]() 【JAVA露营租赁露营装备租赁系统源码支持小程序】实拍 03|我的订单、钱包与租赁押金,展示订单状态、余额构成与押金操作入口;截图数据为演示数据。 |
三、技术架构
这套系统采用三端分离:用户端 uni-app(Vue 语法)编译到微信小程序,管理后台 Vue + ElementUI,服务端 Spring Boot + MyBatis-Plus + MySQL。三端之间只通过 HTTPS API 交互,不共享数据库连接。
一个常见误区是把价格和库存判断放在小程序里,再用接口提交。前端可以做即时反馈,但不能作为最终依据:同一件装备在两个用户同时提交时,只有服务端的条件更新能决定谁成功。
必须在服务端 租金与押金计算、库存扣减、订单状态流转、幂等校验、商家数据范围、审核、支付验签、退款与对账。 | 可以在前端 金额格式化、状态文案与颜色、表单必填提示、图片压缩、空态/骨架屏、本地草稿。 |
接口按 /api/app 和 /api/admin 分组,分别使用用户 token 和管理端 token;管理端接口再做角色和数据范围校验。MyBatis-Plus 负责常规 CRUD,库存扣减、押金流水、复杂报表用自定义 SQL,避免把一致性逻辑藏在通用方法里。
MySQL 使用 InnoDB;订单、库存、押金流水放在同一事务中。缓存、消息队列不是这套基础版的强依赖,部署规模上来后再按实际瓶颈引入,不要为了“架构感”提前增加一致性复杂度。
四、数据库与核心代码
先给结论:金额用分、状态用 TINYINT、订单存快照、唯一键做幂等、押金流水只增不改。这五个决定看起来朴素,但它们决定了后面的事务和退款代码能不能写简单。
-- 金额统一用“分”;状态值由服务端状态机维护CREATE TABLE gear_category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, parent_id BIGINT NOT NULL DEFAULT 0, name VARCHAR(32) NOT NULL, icon_url VARCHAR(255) NOT NULL DEFAULT '', sort_no INT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_parent (parent_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;CREATE TABLE gear_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, merchant_id BIGINT NOT NULL, category_id BIGINT NOT NULL, title VARCHAR(80) NOT NULL, cover_url VARCHAR(255) NOT NULL DEFAULT '', day_price_cent INT NOT NULL COMMENT '日租金,单位分', deposit_cent INT NOT NULL COMMENT '单件押金,单位分', audit_status TINYINT NOT NULL DEFAULT 10 COMMENT '10待审核 20通过 30驳回', sale_status TINYINT NOT NULL DEFAULT 0 COMMENT '1上架 0下架', version INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_merchant (merchant_id), KEY idx_category (category_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;CREATE TABLE gear_stock ( gear_id BIGINT PRIMARY KEY, total_stock INT NOT NULL DEFAULT 0, available_stock INT NOT NULL DEFAULT 0, locked_stock INT NOT NULL DEFAULT 0, rented_stock INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;CREATE TABLE rental_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, request_no VARCHAR(64) NOT NULL COMMENT '客户端幂等号,重试必须复用', user_id BIGINT NOT NULL, merchant_id BIGINT NOT NULL, gear_id BIGINT NOT NULL, gear_title VARCHAR(80) NOT NULL COMMENT '下单快照', quantity INT NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, rent_cent INT NOT NULL, deposit_cent INT NOT NULL, status TINYINT NOT NULL DEFAULT 10 COMMENT '10待支付 20待取货 30租赁中 40待归还 50已完成 60已取消', version INT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), UNIQUE KEY uk_request_no (request_no), KEY idx_user_status (user_id, status), KEY idx_merchant_status (merchant_id, status)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;CREATE TABLE user_wallet ( user_id BIGINT PRIMARY KEY, available_cent INT NOT NULL DEFAULT 0, frozen_cent INT NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;CREATE TABLE deposit_ledger ( id BIGINT PRIMARY KEY AUTO_INCREMENT, biz_no VARCHAR(64) NOT NULL COMMENT '订单号或退款单号', user_id BIGINT NOT NULL, order_id BIGINT NOT NULL, type TINYINT NOT NULL COMMENT '10冻结 20扣减 30退还', amount_cent INT NOT NULL, balance_after_cent INT NOT NULL, remark VARCHAR(255) NOT NULL DEFAULT '', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_biz_type (biz_no, type), KEY idx_user_created (user_id, created_at)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;CREATE TABLE team_member ( id BIGINT PRIMARY KEY AUTO_INCREMENT, merchant_id BIGINT NOT NULL, user_id BIGINT NOT NULL, member_role VARCHAR(16) NOT NULL COMMENT 'OWNER/MANAGER/STAFF/CHECKER', store_id BIGINT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0停用', invited_by BIGINT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_merchant_user (merchant_id, user_id), KEY idx_user (user_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
示例实现,生产环境需补充参数校验、权限、日志、监控、限流、支付验签等。
表结构没有把“分类名、装备标题、日租金、押金”冗余到订单里当作偷懒,而是刻意做快照:历史订单必须能还原下单时的价格和押金口径。索引围绕用户订单列表和商家订单列表设计,避免后台一查就全表扫描。
@Mapperpublic interface GearStockMapper { @Update("UPDATE gear_stock SET available_stock = available_stock - #{qty}, " + "locked_stock = locked_stock + #{qty}, version = version + 1 " + "WHERE gear_id = #{gearId} AND available_stock >= #{qty}") int lockStock(@Param("gearId") Long gearId, @Param("qty") int qty);} |
示例实现,生产环境需补充参数校验、权限、日志、监控、限流、支付验签等。
这段代码只做一件事:把“可租数量足够”与“扣减”合并成一条原子 SQL。InnoDB 会对命中的库存行加锁,返回 1 表示扣减成功,返回 0 表示库存不足或装备不存在。它比“先查库存、再更新”少了一次竞态窗口。
@Servicepublic class RentalOrderService { @Transactional(rollbackFor = Exception.class) public CreateOrderResult create(CreateOrderCmd cmd) { RentalOrder old = orderMapper.selectByRequestNo(cmd.getRequestNo()); if (old != null) { return CreateOrderResult.of(old, true); } GearItem gear = gearMapper.selectById(cmd.getGearId()); if (gear == null || gear.getAuditStatus() != 20 || gear.getSaleStatus() != 1) { throw new BizException("装备未上架或未通过审核"); } int days = (int) ChronoUnit.DAYS.between(cmd.getStartDate(), cmd.getEndDate()); if (days <= 0) { throw new BizException("租期至少为 1 天"); } int qty = cmd.getQuantity(); if (stockMapper.lockStock(gear.getId(), qty) != 1) { throw new BizException("库存不足,请调整租期或数量"); } int rentCent = Math.multiplyExact( Math.multiplyExact(gear.getDayPriceCent(), qty), days); int depositCent = Math.multiplyExact(gear.getDepositCent(), qty); String orderNo = "RV" + IdWorker.getIdStr(); RentalOrder order = new RentalOrder(); order.setOrderNo(orderNo); order.setRequestNo(cmd.getRequestNo()); order.setUserId(cmd.getUserId()); order.setMerchantId(gear.getMerchantId()); order.setGearId(gear.getId()); order.setGearTitle(gear.getTitle()); order.setQuantity(qty); order.setStartDate(cmd.getStartDate()); order.setEndDate(cmd.getEndDate()); order.setRentCent(rentCent); order.setDepositCent(depositCent); order.setStatus(10); orderMapper.insert(order); if (walletMapper.freeze(cmd.getUserId(), depositCent) != 1) { throw new BizException("押金可用余额不足"); } int available = walletMapper.selectAvailable(cmd.getUserId()); depositLedgerMapper.insert(DepositLedger.freeze( orderNo, cmd.getUserId(), order.getId(), depositCent, available)); return CreateOrderResult.of(order, false); }} |
示例实现,生产环境需补充参数校验、权限、日志、监控、限流、支付验签等。
下单事务里,库存锁定、订单写入、押金冻结、流水写入要么一起成功,要么一起回滚。重复请求先按 request_no 查询;并发下由 uk_request_no 兜底,调用方捕获 DuplicateKeyException 后按 request_no 查询并返回已有订单,不会重复扣库存或重复冻结。
@Servicepublic class DepositService { @Transactional(rollbackFor = Exception.class) public void refund(Long orderId, String operator) { RentalOrder order = orderMapper.selectById(orderId); if (order == null || order.getStatus() != 50) { throw new BizException("订单未完成,不能退还押金"); } if (depositLedgerMapper.exists(order.getOrderNo(), 30)) { return; } int amount = order.getDepositCent(); if (walletMapper.unfreeze(order.getUserId(), amount) != 1) { throw new BizException("冻结金额不足,请先核对钱包流水"); } int available = walletMapper.selectAvailable(order.getUserId()); depositLedgerMapper.insert(DepositLedger.refund( order.getOrderNo(), order.getUserId(), orderId, amount, available, operator)); }} |
示例实现,生产环境需补充参数校验、权限、日志、监控、限流、支付验签等。
退款前先查 type=30 的流水,存在就直接返回;真正的并发重复退款由 uk_biz_type 唯一键挡住。若两个请求同时进来,第二个事务会在插入流水时失败并回滚,不会把冻结金额退两次。
@Component("bizPerm")public class BizPermission { private final TeamMemberMapper teamMemberMapper; public BizPermission(TeamMemberMapper teamMemberMapper) { this.teamMemberMapper = teamMemberMapper; } public boolean merchant(Long merchantId) { LoginUser user = SecurityUtils.getLoginUser(); if (user == null) { return false; } if (user.hasRole("PLATFORM_ADMIN")) { return true; } return teamMemberMapper.countActive(merchantId, user.getUserId(), Arrays.asList("OWNER", "MANAGER")) > 0; }}@RestController@RequestMapping("/api/admin/orders")public class AdminOrderController { private final RentalOrderService orderService; @PreAuthorize("@bizPerm.merchant(#merchantId)") @GetMapping public PageResult<OrderVO> page(@RequestParam Long merchantId, OrderQuery query) { return orderService.pageByMerchant(merchantId, query); }} |
示例实现,生产环境需补充参数校验、权限、日志、监控、限流、支付验签等。
权限判断放在方法级注解里,而不是只靠前端菜单。OWNER 和 MANAGER 可以查看商家订单,STAFF 只能处理分配给自己的履约任务;平台管理员走单独角色。服务层查询必须再带 merchant_id 条件,避免“校验通过但查询漏了范围”。
const API_BASE = 'https://api.example.com';const TOKEN_KEY = 'camp_token';export function request({ url, method = 'GET', data = {}, auth = true }) { return new Promise((resolve, reject) => { uni.request({ url: API_BASE + url, method, data, timeout: 15000, header: Object.assign( { 'Content-Type': 'application/json' }, auth ? { Authorization: 'Bearer ' + uni.getStorageSync(TOKEN_KEY) } : {} ), success(res) { if (res.statusCode === 401) { uni.removeStorageSync(TOKEN_KEY); uni.reLaunch({ url: '/pages/login/index' }); reject(new Error('登录已过期')); return; } const body = res.data || {}; if (res.statusCode >= 200 && res.statusCode < 300 && body.code === 0) { resolve(body.data); return; } uni.showToast({ title: body.message || '请求失败', icon: 'none' }); reject(new Error(body.message || 'request failed')); }, fail(err) { uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' }); reject(err); } }); });}// 同一个确认页复用 requestNo;超时重试也不要重新生成let pendingRequestNo = '';export function submitOrder(payload) { if (!pendingRequestNo) { pendingRequestNo = 'req_' + Date.now() + '_' + Math.random().toString(36).slice(2, 10); } return request({ url: '/api/app/orders', method: 'POST', data: Object.assign({}, payload, { requestNo: pendingRequestNo }) }).then((result) => { pendingRequestNo = ''; return result; });} |
示例实现,生产环境需补充参数校验、权限、日志、监控、限流、支付验签等。
多端请求统一封装,登录失效、业务错误和网络异常分别处理。requestNo 在确认页生成一次,超时重试继续复用;只有下单成功后才清空。这样用户连续点击或网络抖动时,不会生成两笔订单。
<template> <div class="gear-audit-page"> <el-form :inline="true" :model="query" size="small"> <el-form-item label="装备"> <el-input v-model="query.keyword" clearable placeholder="名称 / 商家" /> </el-form-item> <el-form-item label="状态"> <el-select v-model="query.auditStatus" clearable placeholder="全部"> <el-option label="待审核" :value="10" /> <el-option label="已通过" :value="20" /> <el-option label="已驳回" :value="30" /> </el-select> </el-form-item> <el-form-item> <el-button type="primary" @click="load(1)">查询</el-button> </el-form-item> </el-form> <el-table v-loading="loading" :data="rows" size="small" border> <el-table-column prop="title" label="装备" min-width="150" show-overflow-tooltip /> <el-table-column prop="merchantName" label="商家" min-width="120" show-overflow-tooltip /> <el-table-column prop="categoryName" label="分类" width="100" /> <el-table-column label="日租金 / 押金" width="140"> <template slot-scope="{ row }"> {{ money(row.dayPriceCent) }} / {{ money(row.depositCent) }} </template> </el-table-column> <el-table-column label="状态" width="96"> <template slot-scope="{ row }"> <el-tag size="small" :type="tagType(row.auditStatus)"> {{ statusText(row.auditStatus) }} </el-tag> </template> </el-table-column> <el-table-column label="操作" width="136" fixed="right"> <template slot-scope="{ row }"> <el-button v-if="row.auditStatus === 10" type="text" @click="openAudit(row)">审核</el-button> <el-button type="text" @click="detail(row)">详情</el-button> </template> </el-table-column> </el-table> <el-pagination background layout="total, prev, pager, next" :total="total" :page-size="query.size" :current-page="query.page" @current-change="load" /> <el-dialog title="装备审核" :visible.sync="auditVisible" width="420px"> <el-form label-width="76px"> <el-form-item label="装备">{{ auditForm.title }}</el-form-item> <el-form-item label="结论"> <el-radio-group v-model="auditForm.pass"> <el-radio :label="true">通过</el-radio> <el-radio :label="false">驳回</el-radio> </el-radio-group> </el-form-item> <el-form-item label="意见"> <el-input v-model="auditForm.remark" type="textarea" :rows="3" maxlength="200" show-word-limit /> </el-form-item> </el-form> <span slot="footer"> <el-button @click="auditVisible = false">取消</el-button> <el-button type="primary" @click="submitAudit">提交</el-button> </span> </el-dialog> </div></template><script>export default { data() { return { loading: false, rows: [], total: 0, query: { page: 1, size: 10, keyword: '', auditStatus: null }, auditVisible: false, auditForm: { id: null, title: '', pass: true, remark: '' } }; }, created() { this.load(1); }, methods: { load(page) { this.query.page = page || this.query.page; this.loading = true; this.$http.get('/api/admin/gears', { params: this.query }) .then((res) => { this.rows = res.data.records; this.total = res.data.total; }) .finally(() => { this.loading = false; }); }, openAudit(row) { this.auditForm = { id: row.id, title: row.title, pass: true, remark: '' }; this.auditVisible = true; }, submitAudit() { if (!this.auditForm.pass && !this.auditForm.remark.trim()) { this.$message.warning('驳回时请填写原因'); return; } this.$http.post('/api/admin/gears/' + this.auditForm.id + '/audit', { pass: this.auditForm.pass, remark: this.auditForm.remark }).then(() => { this.auditVisible = false; this.$message.success('审核已提交'); this.load(this.query.page); }); }, detail(row) { this.$router.push({ path: '/gear/detail', query: { id: row.id } }); }, statusText(status) { return { 10: '待审核', 20: '已通过', 30: '已驳回' }[status] || '未知'; }, tagType(status) { return { 10: 'warning', 20: 'success', 30: 'danger' }[status] || 'info'; }, money(cent) { return '¥' + (Number(cent || 0) / 100).toFixed(2); } }};</script> |
示例实现,生产环境需补充参数校验、权限、日志、监控、限流、支付验签等。
后台表格采用 Vue + ElementUI 的常规写法:查询条件、分页、状态标签、审核弹窗、通过/驳回操作都在同一页完成。审核通过后刷新当前页,避免用户看到旧状态;金额显示统一除以 100,不在模板里做浮点运算。
五、合规与上线检查
露营装备租赁涉及押金、个人信息、商家资质和装备安全,不能只当成一个普通电商项目。下面列出产品与工程侧应落实的检查项,不构成法律意见;上线前请让法务和平台运营按实际业务复核。
工程上线前再做一轮检查:HTTPS、接口鉴权、支付回调验签、退款审批、操作日志、异常监控、限流、数据库备份、押金对账任务。这些不是“加分项”,而是押金类业务能不能长期运行的基础。
六、适合谁
露营地 / 景区自营 有多类帐篷、天幕、桌椅和炊具,需要按日期管理库存、门店自提和押金规则。 | 户外俱乐部 / 租赁门店 需要小程序下单、订单履约、归还验机和复购运营,而不是只做一次性装备买卖。 |
软件公司 / 二开团队 需要 Spring Boot + MyBatis-Plus + uni-app 的底座,按客户业务改分类、押金和商家流程。 | 多门店运营方 需要统一分类、商家中心、团队角色和操作留痕,而不是每个门店各管一套表。 |
如果你只做一次性装备买卖,不需要租期、库存和押金,这套系统会偏重;如果你已经有订单和押金流程,只是缺一个小程序入口,也可以先拆出用户端,再逐步接入商家中心和团队权限。
七、常见问题
Q1:这套系统能直接上线吗?
源码提供的是基础业务能力,上线还取决于经营主体、资质、类目、支付、隐私政策、服务器与备案等条件。可以按部署说明搭建和联调,但不能承诺“包上线”或“保证审核通过”。
Q2:支持哪些端?
用户端使用 uni-app(Vue 语法),按配置编译到微信小程序;管理后台是浏览器端 Vue + ElementUI。H5 也可以编译,但登录、支付和分享能力与小程序不同,需要单独适配。
Q3:押金如何避免重复退款?
订单号 + 流水类型做唯一约束,退款前先查流水,退款事务内更新钱包并写流水;并发重复请求由唯一键兜底。上线后还要做支付渠道对账,不能只信本地账。
Q4:多商家、多门店怎么隔离?
所有商家数据带 merchant_id,服务端从登录态和团队成员关系校验,不信任前端传参;门店范围再叠加 store_id。平台管理员走独立角色,普通商家账号看不到其他商家的订单和结算。
Q5:二开需要什么基础?
需要能读懂 Java、Spring Boot、MyBatis-Plus、MySQL,以及 Vue / uni-app 的基本写法。分类、押金规则、审核流程这类业务改动,通常不需要重写架构;涉及支付、对账和权限的改动,要先补测试。
Q6:截图里的订单是真实数据吗?
不是,全部是演示数据。手机号、门店名称与地址、订单编号已经做不透明覆盖或替换处理;实际部署时请使用自己的测试数据,不要把真实用户信息放进演示环境。


















































































