夜雨聆风学习资料网

ARTICLE · 1358200

JAVA露营租赁露营装备租赁系统源码支持小程序

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 露营装备租赁:把“按天租”算清楚

输入
装备 ID、租期起止日期、数量、取货方式;服务端校验装备是否上架、是否通过审核、库存是否足够。
状态
订单:待支付 → 待取货 → 租赁中 → 待归还 → 已完成/已取消;库存:可租 → 锁定 → 在租 → 归还后回到可租。
输出
带价格快照的订单、取货码、押金冻结记录;商家端看到待履约任务。

装备租赁模块不是商品列表的变体。它要同时回答四个问题:这件装备在所选日期是否可租、租金按几天算、押金按什么标准冻结、归还后库存何时回到可租池。

系统把日租金、押金、可租数量拆成独立字段。下单时服务端重新读取装备状态和库存,不采用前端传来的价格;订单写入装备标题、日租金、押金快照,后续商家改价不会影响历史订单。

库存不是简单的减一。下单先占用可租库存并计入锁定库存,取货后从锁定转入在租,归还验机后再回到可租。未支付订单超时关闭时,在同一事务里释放库存并解冻押金,避免“订单关了、库存还锁着”。

前端负责展示可租日期、价格明细和状态文案;可租判断、金额计算和状态流转都放在服务端。前端禁用按钮只能减少误触,不能替代后端的并发控制。

【JAVA露营租赁露营装备租赁系统源码支持小程序】实拍 01|首页推荐、分类筛选与租物车,对应装备发现、分类浏览与下单前确认;截图数据为演示数据。

02 分类管理:让运营改类目不用发版

输入
分类名称、父级分类、图标、排序、启停状态;装备发布时绑定分类 ID。
状态
启用/停用、分类移动;停用后不再出现在用户端筛选,历史订单保留分类名称快照。
输出
分类树接口、首页筛选标签、后台分类维护页面。

分类是运营工具,不是前端写死的 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 交互,不共享数据库连接。

用户端
uni-app(Vue 语法)→ 微信小程序。负责首页、分类、装备详情、租物车、确认订单、地址、订单与押金页面;处理登录态、表单交互、图片上传和支付唤起。
管理后台
Vue + ElementUI。负责装备审核、分类维护、库存调整、订单履约、商家管理、团队成员、押金规则与报表;表格分页、状态标签、审核弹窗是主要交互。
服务端
Spring Boot + MyBatis-Plus + MySQL。负责价格与押金计算、库存扣减、订单状态机、幂等、权限与数据范围、审核、支付回调、退款与对账。

一个常见误区是把价格和库存判断放在小程序里,再用接口提交。前端可以做即时反馈,但不能作为最终依据:同一件装备在两个用户同时提交时,只有服务端的条件更新能决定谁成功。

必须在服务端

租金与押金计算、库存扣减、订单状态流转、幂等校验、商家数据范围、审核、支付验签、退款与对账。

可以在前端

金额格式化、状态文案与颜色、表单必填提示、图片压缩、空态/骨架屏、本地草稿。

接口按 /api/app 和 /api/admin 分组,分别使用用户 token 和管理端 token;管理端接口再做角色和数据范围校验。MyBatis-Plus 负责常规 CRUD,库存扣减、押金流水、复杂报表用自定义 SQL,避免把一致性逻辑藏在通用方法里。

MySQL 使用 InnoDB;订单、库存、押金流水放在同一事务中。缓存、消息队列不是这套基础版的强依赖,部署规模上来后再按实际瓶颈引入,不要为了“架构感”提前增加一致性复杂度。

四、数据库与核心代码

先给结论:金额用分、状态用 TINYINT、订单存快照、唯一键做幂等、押金流水只增不改。这五个决定看起来朴素,但它们决定了后面的事务和退款代码能不能写简单。

gear_item 1—1 gear_stock
装备与库存分开,库存行用条件更新控制并发。
rental_order 1—N deposit_ledger
一笔订单的冻结、扣减、退还都进流水,不直接改历史记录。
merchant 1—N team_member
团队成员绑定商家与角色,接口按 merchant_id + role 校验。
user 1—N user_address
地址软删除、默认地址唯一、订单保存地址快照。
SQL
-- 金额统一用“分”;状态值由服务端状态机维护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;

示例实现,生产环境需补充参数校验、权限、日志、监控、限流、支付验签等。

表结构没有把“分类名、装备标题、日租金、押金”冗余到订单里当作偷懒,而是刻意做快照:历史订单必须能还原下单时的价格和押金口径。索引围绕用户订单列表和商家订单列表设计,避免后台一查就全表扫描。

JAVA
@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 表示库存不足或装备不存在。它比“先查库存、再更新”少了一次竞态窗口。

JAVA
@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 查询并返回已有订单,不会重复扣库存或重复冻结。

JAVA
@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 唯一键挡住。若两个请求同时进来,第二个事务会在插入流水时失败并回滚,不会把冻结金额退两次。

JAVA
@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 条件,避免“校验通过但查询漏了范围”。

JAVASCRIPT
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 在确认页生成一次,超时重试继续复用;只有下单成功后才清空。这样用户连续点击或网络抖动时,不会生成两笔订单。

VUE
<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,不在模板里做浮点运算。

五、合规与上线检查

露营装备租赁涉及押金、个人信息、商家资质和装备安全,不能只当成一个普通电商项目。下面列出产品与工程侧应落实的检查项,不构成法律意见;上线前请让法务和平台运营按实际业务复核。

押金与资金
押金通过持牌支付机构收取与退回,平台不形成资金池;规则在下单前公示,退款原路退回,保留对账与退款时效记录。涉及预收租金或储值时,另行评估资金存管要求。
实名与未成年人
实名信息只在必要场景收集,避免过度收集身份证号;涉及炉具、刀具等装备时设置年龄提示与线下核验,未成年人租用需监护人确认。
个人信息
手机号、地址加密存储,展示与日志脱敏;明确收集目的、保存期限和共享范围,提供注销、删除、导出渠道;第三方 SDK 在隐私政策中列明。
商家与装备
核验营业执照与经营资质;建立装备检查、消毒、维护记录;明确损坏判定、保险与事故处理流程,避免仅凭商家单方描述扣减押金。
小程序规则
按实际业务选择类目,完成支付、隐私协议、用户协议配置;不诱导分享、不虚假宣传;团队分佣避免多层级和拉人头模式。
营销表述
截图标注演示数据;不承诺收益、流量、审核通过或“包上线”;不使用“官方直连”“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:截图里的订单是真实数据吗?

不是,全部是演示数据。手机号、门店名称与地址、订单编号已经做不透明覆盖或替换处理;实际部署时请使用自己的测试数据,不要把真实用户信息放进演示环境。

八、结语

如果你正在做露营地、户外俱乐部或装备租赁门店,或想基于 Spring Boot + MyBatis-Plus + MySQL、uni-app、Vue + ElementUI 做一套可二开的露营装备租赁系统,欢迎找我领取演示资料和部署说明。

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料