JAVA无人共享台球杆台球柜系统球杆柜租赁系统源码支持微信小程序

JAVA无人共享台球杆台球柜系统:智能体育器材租赁解决方案(支持微信小程序)

在共享经济3.0时代和体育产业数字化浪潮的双重驱动下,JAVA无人共享台球杆台球柜系统 正成为体育器材租赁行业数字化转型的重要技术基础设施。根据国家体育总局数据显示,2023年中国台球运动人口已突破8000万,年复合增长率达15%,而专业台球器材的购置成本高昂,成为制约运动普及的重要因素。这套球杆柜租赁系统源码通过物联网技术和智能管理平台,有效解决了传统台球器材租赁存在的管理效率低、器材损耗大、用户体验差等结构性痛点。

从技术经济学角度分析,该系统采用SpringBoot+MyBatisPlus+MySQL 的微服务分布式架构,实现了设备状态实时监控和租赁业务的高并发处理。基于Uniapp 框架构建的微信小程序客户端,使运营方能够快速触达微信生态的10亿用户,极大降低了用户获取成本。管理后台采用Vue+ElementUI技术栈,为运营商提供了智能化的设备管理和数据分析能力。

行业前景方面,随着智能物联网技术的成熟和共享经济模式的深化,体育器材共享市场正朝着智能化、网络化、标准化方向发展。艾瑞咨询研究报告指出,智能共享器材租赁系统的应用可使设备利用率提升50%以上,运营成本降低40%。这套JAVA无人共享台球杆台球柜系统不仅实现了租赁流程的数字化重构,更通过智能调度算法和预防性维护机制为运营商提供了科学的设备管理和维护策略,将成为体育器材共享行业标准化发展的重要技术支撑。

系统技术架构深度解析 分布式租赁服务架构设计

本系统采用基于SpringBoot 的微服务架构,结合MyBatisPlusMySQL构建高可用的租赁服务集群。以下是核心业务架构代码示例:

复制代码
// 台球杆设备实体类
@Entity
@Table(name = "billiards_cue")
public class BilliardsCue {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "cue_sn", unique = true, nullable = false)
    private String cueSn; // 球杆唯一编号
    
    @Column(name = "cue_brand")
    private String cueBrand; // 球杆品牌
    
    @Column(name = "cue_model")
    private String cueModel; // 球杆型号
    
    @Column(name = "cue_weight")
    private BigDecimal cueWeight; // 球杆重量
    
    @Column(name = "cue_tip_size")
    private BigDecimal cueTipSize; // 杆头尺寸
    
    @Column(name = "current_status")
    private Integer currentStatus; // 当前状态:0-空闲 1-租赁中 2-维护中
    
    @Column(name = "cabinet_id")
    private Long cabinetId; // 所属柜子ID
    
    @Column(name = "slot_position")
    private Integer slotPosition; // 槽位位置
    
    @Column(name = "rental_count")
    private Integer rentalCount = 0; // 租赁次数
    
    @Column(name = "last_maintenance_date")
    private LocalDate lastMaintenanceDate; // 最后维护日期
    
    @Column(name = "battery_level")
    private Integer batteryLevel; // 电量(智能球杆)
    
    @Column(name = "create_time")
    private LocalDateTime createTime;
}

// 智能柜实体
@Entity
@Table(name = "smart_cabinet")
public class SmartCabinet {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "cabinet_sn", unique = true, nullable = false)
    private String cabinetSn; // 柜子序列号
    
    @Column(name = "cabinet_name")
    private String cabinetName; // 柜子名称
    
    @Column(name = "location")
    private String location; // 安装位置
    
    @Column(name = "geo_location")
    private String geoLocation; // 地理坐标
    
    @Column(name = "total_slots")
    private Integer totalSlots; // 总槽位数
    
    @Column(name = "available_slots")
    private Integer availableSlots; // 可用槽位
    
    @Column(name = "cabinet_status")
    private Integer cabinetStatus; // 柜子状态
    
    @Column(name = "network_status")
    private Integer networkStatus; // 网络状态
    
    @Column(name = "power_status")
    private Integer powerStatus; // 电源状态
    
    @Column(name = "last_heartbeat")
    private LocalDateTime lastHeartbeat; // 最后心跳时间
}

// 智能调度服务
@Service
public class CueDispatchService {
    
    @Autowired
    private BilliardsCueMapper cueMapper;
    
    @Autowired
    private SmartCabinetMapper cabinetMapper;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 智能推荐可用球杆
     */
    public List<CueRecommendationDTO> recommendCues(Long userId, String userLocation) {
        // 1. 获取附近可用的智能柜
        List<SmartCabinet> nearbyCabinets = findNearbyCabinets(userLocation, 5000); // 5公里范围内
        
        // 2. 获取用户偏好
        UserPreference preference = getUserPreference(userId);
        
        // 3. 多维度推荐排序
        return nearbyCabinets.stream()
                .flatMap(cabinet -> getAvailableCues(cabinet.getId()).stream())
                .map(cue -> {
                    RecommendationScore score = calculateRecommendationScore(cue, preference);
                    return new CueRecommendationDTO(cue, score);
                })
                .sorted(Comparator.comparing(CueRecommendationDTO::getScore).reversed())
                .collect(Collectors.toList());
    }
    
    /**
     * 计算球杆推荐分数
     */
    private RecommendationScore calculateRecommendationScore(BilliardsCue cue, UserPreference preference) {
        RecommendationScore score = new RecommendationScore();
        
        // 设备状态分数(40%权重)
        score.setDeviceStatusScore(calculateDeviceStatusScore(cue));
        
        // 用户偏好匹配度(30%权重)
        score.setPreferenceMatchScore(calculatePreferenceMatchScore(cue, preference));
        
        // 设备使用频率(20%权重)
        score.setUsageScore(calculateUsageScore(cue));
        
        // 维护状态(10%权重)
        score.setMaintenanceScore(calculateMaintenanceScore(cue));
        
        return score;
    }
    
    /**
     * 计算设备状态分数
     */
    private double calculateDeviceStatusScore(BilliardsCue cue) {
        double score = 0;
        
        // 基础状态分数
        if (cue.getCurrentStatus() == 0) {
            score += 60; // 空闲状态
        }
        
        // 电量分数(智能球杆)
        if (cue.getBatteryLevel() != null) {
            score += cue.getBatteryLevel() * 0.4;
        }
        
        // 使用次数分数(适度使用的最优)
        if (cue.getRentalCount() < 100) {
            score += 20;
        } else if (cue.getRentalCount() < 500) {
            score += 15;
        } else {
            score += 5;
        }
        
        return score;
    }
}

// 推荐评分模型
@Data
class RecommendationScore {
    private double deviceStatusScore;     // 设备状态分数
    private double preferenceMatchScore;  // 偏好匹配度
    private double usageScore;            // 使用频率分数
    private double maintenanceScore;      // 维护状态分数
    
    public double getScore() {
        return deviceStatusScore * 0.4 + preferenceMatchScore * 0.3 + 
               usageScore * 0.2 + maintenanceScore * 0.1;
    }
}
租赁订单状态机引擎
复制代码
// 租赁订单实体
@Entity
@Table(name = "cue_rental_order")
public class CueRentalOrder {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    
    @Column(name = "order_no", unique = true)
    private String orderNo; // 订单编号
    
    @Column(name = "user_id")
    private Long userId; // 用户ID
    
    @Column(name = "cue_id")
    private Long cueId; // 球杆ID
    
    @Column(name = "cabinet_id")
    private Long cabinetId; // 柜子ID
    
    @Column(name = "rental_start_time")
    private LocalDateTime rentalStartTime; // 租赁开始时间
    
    @Column(name = "rental_end_time")
    private LocalDateTime rentalEndTime; // 租赁结束时间
    
    @Column(name = "actual_return_time")
    private LocalDateTime actualReturnTime; // 实际归还时间
    
    @Column(name = "rental_duration")
    private Integer rentalDuration; // 租赁时长(分钟)
    
    @Column(name = "total_amount")
    private BigDecimal totalAmount; // 订单总额
    
    @Column(name = "payment_status")
    private Integer paymentStatus; // 支付状态
    
    @Column(name = "order_status")
    private Integer orderStatus; // 订单状态
    
    @Column(name = "create_time")
    private LocalDateTime createTime;
}

// 租赁订单服务
@Service
@Transactional
public class CueRentalService {
    
    @Autowired
    private CueRentalOrderMapper orderMapper;
    
    @Autowired
    private BilliardsCueMapper cueMapper;
    
    @Autowired
    private SmartCabinetMapper cabinetMapper;
    
    @Autowired
    private IotDeviceService iotDeviceService;
    
    @Autowired
    private NotificationService notificationService;
    
    /**
     * 创建租赁订单
     */
    public CueRentalOrder createRentalOrder(RentalOrderDTO orderDTO) {
        // 1. 验证设备可用性
        BilliardsCue cue = cueMapper.selectById(orderDTO.getCueId());
        if (cue.getCurrentStatus() != 0) {
            throw new RentalException("球杆暂不可用");
        }
        
        // 2. 创建订单记录
        CueRentalOrder order = new CueRentalOrder();
        BeanUtils.copyProperties(orderDTO, order);
        order.setOrderNo(generateOrderNo());
        order.setOrderStatus(1); // 待取杆
        order.setPaymentStatus(1); // 待支付
        order.setCreateTime(LocalDateTime.now());
        orderMapper.insert(order);
        
        // 3. 更新球杆状态
        cue.setCurrentStatus(1); // 租赁中
        cueMapper.updateById(cue);
        
        // 4. 控制智能柜开锁
        boolean openResult = iotDeviceService.openCabinetSlot(
            orderDTO.getCabinetId(), 
            cue.getSlotPosition()
        );
        
        if (!openResult) {
            throw new RentalException("设备开锁失败,请重试");
        }
        
        // 5. 发送订单创建通知
        notificationService.sendRentalStartNotification(order);
        
        return order;
    }
    
    /**
     * 归还球杆处理
     */
    public boolean returnCue(String orderNo, Long cabinetId, Integer slotPosition) {
        CueRentalOrder order = getOrderByNo(orderNo);
        
        // 状态机验证
        if (order.getOrderStatus() != 2) {
            throw new RentalException("订单状态异常");
        }
        
        // 1. 更新球杆状态和位置
        BilliardsCue cue = cueMapper.selectById(order.getCueId());
        cue.setCurrentStatus(0); // 空闲
        cue.setCabinetId(cabinetId);
        cue.setSlotPosition(slotPosition);
        cue.setRentalCount(cue.getRentalCount() + 1);
        cueMapper.updateById(cue);
        
        // 2. 更新订单状态
        order.setOrderStatus(3); // 已完成
        order.setActualReturnTime(LocalDateTime.now());
        order.setRentalDuration(calculateRentalDuration(
            order.getRentalStartTime(), 
            order.getActualReturnTime()
        ));
        orderMapper.updateById(order);
        
        // 3. 计算费用
        calculateRentalFee(order);
        
        // 4. 发送归还成功通知
        notificationService.sendReturnSuccessNotification(order);
        
        return true;
    }
    
    /**
     * 计算租赁时长(分钟)
     */
    private Integer calculateRentalDuration(LocalDateTime startTime, LocalDateTime endTime) {
        return (int) Duration.between(startTime, endTime).toMinutes();
    }
    
    /**
     * 计算租赁费用
     */
    private void calculateRentalFee(CueRentalOrder order) {
        // 基础计费规则:首小时X元,之后每半小时Y元
        BigDecimal basePrice = new BigDecimal("10.00"); // 首小时价格
        BigDecimal unitPrice = new BigDecimal("5.00");  // 后续半小时价格
        
        int totalMinutes = order.getRentalDuration();
        int baseMinutes = 60; // 首小时
        int unitMinutes = 30; // 计费单元
        
        if (totalMinutes <= baseMinutes) {
            order.setTotalAmount(basePrice);
        } else {
            int extraMinutes = totalMinutes - baseMinutes;
            int extraUnits = (int) Math.ceil((double) extraMinutes / unitMinutes);
            order.setTotalAmount(basePrice.add(unitPrice.multiply(BigDecimal.valueOf(extraUnits))));
        }
        
        orderMapper.updateById(order);
    }
    
    private String generateOrderNo() {
        return "RO" + System.currentTimeMillis() + 
               RandomUtil.randomNumbers(6);
    }
}
微信小程序Uniapp实现

用户端采用Uniapp开发,支持微信小程序。以下是核心租赁功能页面实现:

复制代码
<template>
  <view class="cue-rental-container">
    <!-- 顶部定位和搜索 -->
    <view class="header-section">
      <view class="location-selector" @tap="chooseLocation">
        <image src="/static/location-icon.png" class="location-icon"></image>
        <text class="location-text">{{ currentLocation }}</text>
        <image src="/static/arrow-down.png" class="arrow-icon"></image>
      </view>
      <view class="search-box" @tap="goToSearch">
        <image src="/static/search-icon.png" class="search-icon"></image>
        <text class="search-placeholder">搜索台球厅或位置</text>
      </view>
    </view>

    <!-- 附近智能柜 -->
    <view class="nearby-cabinets-section">
      <view class="section-title">附近智能柜</view>
      <view class="cabinet-list">
        <view v-for="cabinet in nearbyCabinets" :key="cabinet.id" 
              class="cabinet-card" @tap="viewCabinetDetail(cabinet)">
          <view class="cabinet-header">
            <image src="/static/cabinet-icon.png" class="cabinet-icon"></image>
            <view class="cabinet-info">
              <text class="cabinet-name">{{ cabinet.cabinetName }}</text>
              <view class="cabinet-status">
                <view class="status-indicator" :class="getStatusClass(cabinet.cabinetStatus)"></view>
                <text class="status-text">{{ getStatusText(cabinet.cabinetStatus) }}</text>
              </view>
            </view>
            <view class="distance-badge">
              <text>{{ cabinet.distance }}m</text>
            </view>
          </view>
          
          <view class="cabinet-details">
            <view class="detail-item">
              <image src="/static/location-small.png" class="detail-icon"></image>
              <text class="detail-text">{{ cabinet.location }}</text>
            </view>
            <view class="detail-item">
              <image src="/static/cue-icon.png" class="detail-icon"></image>
              <text class="detail-text">可用球杆: {{ cabinet.availableSlots }}/{{ cabinet.totalSlots }}</text>
            </view>
            <view class="detail-item">
              <image src="/static/price-icon.png" class="detail-icon"></image>
              <text class="detail-text">首小时¥10,之后每半小时¥5</text>
            </view>
          </view>
          
          <view class="cabinet-actions">
            <button class="nav-btn" @tap.stop="openNavigation(cabinet)">
              <image src="/static/navigation.png" class="btn-icon"></image>
              导航
            </button>
            <button class="rent-btn" @tap.stop="quickRent(cabinet)">
              立即租杆
            </button>
          </view>
        </view>
      </view>
    </view>

    <!-- 智能柜详情模态框 -->
    <uni-popup ref="cabinetPopup" type="bottom">
      <view class="cabinet-detail-modal">
        <view class="modal-header">
          <text class="modal-title">{{ selectedCabinet.cabinetName }}</text>
          <text class="close-btn" @tap="closeCabinetModal">×</text>
        </view>
        
        <view class="cabinet-status-overview">
          <view class="status-card">
            <text class="status-value">{{ selectedCabinet.availableSlots }}</text>
            <text class="status-label">可用球杆</text>
          </view>
          <view class="status-card">
            <text class="status-value">{{ selectedCabinet.totalSlots - selectedCabinet.availableSlots }}</text>
            <text class="status-label">已租出</text>
          </view>
          <view class="status-card">
            <text class="status-value">{{ getUptime(selectedCabinet.lastHeartbeat) }}</text>
            <text class="status-label">运行状态</text>
          </view>
        </view>
        
        <!-- 球杆列表 -->
        <view class="cue-list-section">
          <text class="section-title">可用球杆</text>
          <scroll-view class="cue-list" scroll-y>
            <view v-for="cue in availableCues" :key="cue.id" 
                  class="cue-item" :class="{recommended: cue.isRecommended}">
              <view class="cue-info">
                <image src="/static/cue.png" class="cue-icon"></image>
                <view class="cue-details">
                  <text class="cue-brand">{{ cue.cueBrand }} {{ cue.cueModel }}</text>
                  <view class="cue-specs">
                    <text class="spec-item">重量: {{ cue.cueWeight }}oz</text>
                    <text class="spec-item">杆头: {{ cue.cueTipSize }}mm</text>
                    <text class="spec-item">租赁: {{ cue.rentalCount }}次</text>
                  </view>
                  <view class="cue-status">
                    <text class="status-tag" :class="getCueStatusClass(cue)">
                      {{ getCueStatusText(cue) }}
                    </text>
                    <text class="battery-level" v-if="cue.batteryLevel">
                      电量: {{ cue.batteryLevel }}%
                    </text>
                  </view>
                </view>
              </view>
              <button class="select-btn" @tap="selectCue(cue)" 
                      :disabled="cue.currentStatus !== 0">
                {{ cue.currentStatus === 0 ? '选择' : '不可用' }}
              </button>
            </view>
          </scroll-view>
        </view>
        
        <button class="confirm-rent-btn" @tap="confirmRental" 
                :disabled="!selectedCue">确认租赁</button>
      </view>
    </uni-popup>

    <!-- 租赁确认模态框 -->
    <uni-popup ref="confirmPopup" type="center">
      <view class="confirm-rental-modal">
        <text class="modal-title">确认租赁</text>
        
        <view class="rental-info">
          <view class="info-item">
            <text class="info-label">球杆信息:</text>
            <text class="info-value">{{ selectedCue.cueBrand }} {{ selectedCue.cueModel }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">取杆位置:</text>
            <text class="info-value">{{ selectedCabinet.location }}</text>
          </view>
          <view class="info-item">
            <text class="info-label">计费规则:</text>
            <text class="info-value">首小时¥10,之后每半小时¥5</text>
          </view>
        </view>
        
        <view class="rental-agreement">
          <checkbox-group @change="onAgreementChange">
            <label class="agreement-item">
              <checkbox :checked="agreementChecked" />
              <text class="agreement-text">我已阅读并同意《球杆租赁协议》</text>
            </label>
          </checkbox-group>
        </view>
        
        <view class="modal-actions">
          <button class="cancel-btn" @tap="cancelRental">取消</button>
          <button class="confirm-btn" @tap="createRentalOrder" 
                  :disabled="!agreementChecked">确认租赁</button>
        </view>
      </view>
    </uni-popup>

    <!-- 租赁中状态面板 -->
    <view class="renting-panel" v-if="currentRental">
      <view class="panel-header">
        <text class="panel-title">租赁中</text>
        <text class="rental-duration">{{ formatDuration(currentRental.duration) }}</text>
      </view>
      
      <view class="rental-info">
        <text class="info-text">球杆: {{ currentRental.cueInfo }}</text>
        <text class="info-text">位置: {{ currentRental.cabinetLocation }}</text>
        <text class="info-text">当前费用: ¥{{ currentRental.currentFee }}</text>
      </view>
      
      <view class="action-buttons">
        <button class="extend-btn" @tap="extendRental">续租</button>
        <button class="return-btn" @tap="showReturnGuide">归还</button>
      </view>
    </view>

    <!-- 底部导航 -->
    <view class="tabbar">
      <view class="tabbar-item" :class="{active: currentTab === 0}" 
            @tap="switchTab(0)">
        <image :src="currentTab === 0 ? '/static/home-active.png' : '/static/home.png'" 
               class="tab-icon"></image>
        <text class="tab-text">首页</text>
      </view>
      <view class="tabbar-item" :class="{active: currentTab === 1}" 
            @tap="switchTab(1)">
        <image :src="currentTab === 1 ? '/static/order-active.png' : '/static/order.png'" 
               class="tab-icon"></image>
        <text class="tab-text">订单</text>
      </view>
      <view class="tabbar-item" :class="{active: currentTab === 2}" 
            @tap="switchTab(2)">
        <image :src="currentTab === 2 ? '/static/profile-active.png' : '/static/profile.png'" 
               class="tab-icon"></image>
        <text class="tab-text">我的</text>
      </view>
    </view>
  </view>
</template>

<script>
export default {
  data() {
    return {
      currentTab: 0,
      currentLocation: '定位中...',
      nearbyCabinets: [],
      selectedCabinet: {},
      availableCues: [],
      selectedCue: null,
      currentRental: null,
      agreementChecked: false,
      
      // 租赁计时器
      rentalTimer: null,
      rentalStartTime: null
    }
  },
  
  computed: {
    canRent() {
      return this.selectedCue && this.agreementChecked
    }
  },
  
  onLoad() {
    this.getCurrentLocation()
    this.loadNearbyCabinets()
    this.checkCurrentRental()
  },
  
  onUnload() {
    this.clearRentalTimer()
  },
  
  methods: {
    // 获取当前位置
    async getCurrentLocation() {
      try {
        const location = await this.getUserLocation()
        const address = await this.reverseGeocode(location.latitude, location.longitude)
        this.currentLocation = address
      } catch (error) {
        this.currentLocation = '定位失败'
      }
    },
    
    // 加载附近智能柜
    async loadNearbyCabinets() {
      try {
        const location = await this.getUserLocation()
        const params = {
          latitude: location.latitude,
          longitude: location.longitude,
          radius: 5000
        }
        
        const res = await this.$http.get('/api/cabinet/nearby', { params })
        
        if (res.code === 200) {
          this.nearbyCabinets = res.data
        }
      } catch (error) {
        uni.showToast({
          title: '加载失败',
          icon: 'none'
        })
      }
    },
    
    // 查看柜子详情
    async viewCabinetDetail(cabinet) {
      this.selectedCabinet = cabinet
      try {
        const res = await this.$http.get(`/api/cabinet/${cabinet.id}/cues`)
        if (res.code === 200) {
          this.availableCues = res.data
          this.$refs.cabinetPopup.open()
        }
      } catch (error) {
        uni.showToast({
          title: '加载球杆信息失败',
          icon: 'none'
        })
      }
    },
    
    // 选择球杆
    selectCue(cue) {
      this.selectedCue = cue
    },
    
    // 确认租赁
    confirmRental() {
      if (!this.selectedCue) {
        uni.showToast({
          title: '请选择球杆',
          icon: 'none'
        })
        return
      }
      
      this.$refs.confirmPopup.open()
    },
    
    // 创建租赁订单
    async createRentalOrder() {
      try {
        const orderData = {
          cueId: this.selectedCue.id,
          cabinetId: this.selectedCabinet.id
        }
        
        const res = await this.$http.post('/api/rental/create', orderData)
        
        if (res.code === 200) {
          uni.showToast({
            title: '租赁成功',
            icon: 'success'
          })
          
          this.closeAllModals()
          this.startRentalTimer(res.data)
        }
      } catch (error) {
        uni.showToast({
          title: error.message || '租赁失败',
          icon: 'none'
        })
      }
    },
    
    // 开始租赁计时
    startRentalTimer(rentalOrder) {
      this.currentRental = {
        id: rentalOrder.id,
        cueInfo: `${this.selectedCue.cueBrand} ${this.selectedCue.cueModel}`,
        cabinetLocation: this.selectedCabinet.location,
        startTime: new Date(),
        duration: 0,
        currentFee: '0.00'
      }
      
      this.rentalStartTime = new Date()
      this.rentalTimer = setInterval(() => {
        this.updateRentalInfo()
      }, 60000) // 每分钟更新一次
    },
    
    // 更新租赁信息
    updateRentalInfo() {
      const now = new Date()
      const duration = Math.floor((now - this.rentalStartTime) / 60000) // 分钟
      
      this.currentRental.duration = duration
      this.currentRental.currentFee = this.calculateCurrentFee(duration)
    },
    
    // 计算当前费用
    calculateCurrentFee(duration) {
      const basePrice = 10.00 // 首小时
      const unitPrice = 5.00  // 每半小时
      
      if (duration <= 60) {
        return basePrice.toFixed(2)
      } else {
        const extraUnits = Math.ceil((duration - 60) / 30)
        return (basePrice + unitPrice * extraUnits).toFixed(2)
      }
    },
    
    // 归还球杆
    async showReturnGuide() {
      uni.showModal({
        title: '归还球杆',
        content: '请将球杆放回任意智能柜的空闲槽位,系统将自动结束计费',
        confirmText: '确认归还',
        success: async (res) => {
          if (res.confirm) {
            await this.returnCue()
          }
        }
      })
    },
    
    // 执行归还操作
    async returnCue() {
      try {
        const res = await this.$http.post('/api/rental/return', {
          orderId: this.currentRental.id
        })
        
        if (res.code === 200) {
          uni.showToast({
            title: '归还成功',
            icon: 'success'
          })
          
          this.clearRentalTimer()
          this.currentRental = null
          this.loadNearbyCabinets() // 刷新柜子状态
        }
      } catch (error) {
        uni.showToast({
          title: '归还失败',
          icon: 'none'
        })
      }
    },
    
    // 检查当前是否有租赁中的订单
    async checkCurrentRental() {
      try {
        const res = await this.$http.get('/api/rental/current')
        if (res.code === 200 && res.data) {
          this.startRentalTimer(res.data)
        }
      } catch (error) {
        console.error('检查当前租赁失败', error)
      }
    },
    
    // 清理计时器
    clearRentalTimer() {
      if (this.rentalTimer) {
        clearInterval(this.rentalTimer)
        this.rentalTimer = null
      }
    },
    
    // 关闭所有模态框
    closeAllModals() {
      this.$refs.cabinetPopup.close()
      this.$refs.confirmPopup.close()
    },
    
    // 获取状态样式
    getStatusClass(status) {
      const classMap = {
        1: 'online',
        2: 'offline',
        3: 'maintenance'
      }
      return classMap[status] || 'offline'
    },
    
    // 获取状态文本
    getStatusText(status) {
      const textMap = {
        1: '正常',
        2: '离线',
        3: '维护中'
      }
      return textMap[status] || '未知'
    },
    
    // 格式化时长
    formatDuration(minutes) {
      const hours = Math.floor(minutes / 60)
      const mins = minutes % 60
      return `${hours.toString().padStart(2, '0')}:${mins.toString().padStart(2, '0')}`
    }
  }
}
</script>

<style scoped>
.cue-rental-container {
  background: #f5f7fa;
  min-height: 100vh;
  padding-bottom: 120rpx;
}

.header-section {
  display: flex;
  align-items: center;
  padding: 30rpx;
  background: white;
}

.cabinet-card {
  background: white;
  margin: 20rpx;
  border-radius: 20rpx;
  padding: 30rpx;
  box-shadow: 0 4rpx 20rpx rgba(0,0,0,0.08);
}

.cabinet-header {
  display: flex;
  align-items: center;
  margin-bottom: 20rpx;
}

.status-indicator {
  width: 20rpx;
  height: 20rpx;
  border-radius: 50%;
  margin-right: 10rpx;
}

.status-indicator.online {
  background: #52c41a;
}

.status-indicator.offline {
  background: #ff4d4f;
}

.status-indicator.maintenance {
  background: #faad14;
}

.cabinet-actions {
  display: flex;
  justify-content: space-between;
  margin-top: 30rpx;
}

.nav-btn, .rent-btn {
  flex: 1;
  margin: 0 10rpx;
  border-radius: 10rpx;
  padding: 20rpx;
}

.rent-btn {
  background: #1890ff;
  color: white;
}

.renting-panel {
  position: fixed;
  bottom: 120rpx;
  left: 20rpx;
  right: 20rpx;
  background: white;
  border-radius: 20rpx;
  padding: 30rpx;
  box-shadow: 0 -4rpx 20rpx rgba(0,0,0,0.1);
}

.confirm-rental-modal {
  background: white;
  border-radius: 20rpx;
  padding: 40rpx;
  margin: 40rpx;
}

.agreement-item {
  display: flex;
  align-items: center;
  margin: 20rpx 0;
}

.tabbar {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  background: white;
  display: flex;
  padding: 20rpx 0;
  border-top: 1rpx solid #eee;
}

.tabbar-item {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
}
</style>
管理后台Vue+ElementUI实现

管理后台采用Vue+ElementUI构建,提供完善的设备管理和运营分析功能:

复制代码
<template>
  <div class="cue-management-container">
    <!-- 数据概览 -->
    <el-row :gutter="20" class="stats-row">
      <el-col :span="6">
        <el-card class="stats-card">
          <div class="stats-content">
            <div class="stats-number">{{ stats.totalCabinets }}</div>
            <div class="stats-label">总柜数</div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card class="stats-card">
          <div class="stats-content">
            <div class="stats-number">{{ stats.totalCues }}</div>
            <div class="stats-label">总球杆数</div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card class="stats-card">
          <div class="stats-content">
            <div class="stats-number">{{ stats.todayRentals }}</div>
            <div class="stats-label">今日租赁</div>
          </div>
        </el-card>
      </el-col>
      <el-col :span="6">
        <el-card class="stats-card">
          <div class="stats-content">
            <div class="stats-number">¥{{ stats.todayRevenue }}</div>
            <div class="stats-label">今日收入</div>
          </div>
        </el-card>
      </el-col>
    </el-row>

    <!-- 智能柜管理 -->
    <el-card class="table-card">
      <template #header>
        <div class="card-header">
          <span>智能柜管理</span>
          <el-button type="primary" @click="addCabinet">新增柜子</el-button>
        </div>
      </template>
      
      <!-- 筛选条件 -->
      <el-form :model="queryParams" inline>
        <el-form-item label="柜子状态">
          <el-select v-model="queryParams.cabinetStatus" placeholder="请选择状态">
            <el-option label="全部" value=""></el-option>
            <el-option label="正常" value="1"></el-option>
            <el-option label="离线" value="2"></el-option>
            <el-option label="维护中" value="3"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="网络状态">
          <el-select v-model="queryParams.networkStatus" placeholder="请选择状态">
            <el-option label="全部" value=""></el-option>
            <el-option label="在线" value="1"></el-option>
            <el-option label="离线" value="2"></el-option>
          </el-select>
        </el-form-item>
        <el-form-item>
          <el-button type="primary" @click="handleSearch">搜索</el-button>
          <el-button @click="handleReset">重置</el-button>
        </el-form-item>
      </el-form>
      
      <!-- 柜子表格 -->
      <el-table :data="cabinetList" v-loading="loading">
        <el-table-column prop="id" label="ID" width="80" />
        <el-table-column prop="cabinetName" label="柜子名称" width="150" />
        <el-table-column prop="cabinetSn" label="序列号" width="180" />
        <el-table-column prop="location" label="安装位置" width="200" />
        <el-table-column prop="availableSlots" label="可用槽位" width="120">
          <template #default="scope">
            {{ scope.row.availableSlots }}/{{ scope.row.totalSlots }}
          </template>
        </el-table-column>
        <el-table-column prop="cabinetStatus" label="柜子状态" width="120">
          <template #default="scope">
            <el-tag :type="getCabinetStatusTagType(scope.row.cabinetStatus)">
              {{ getCabinetStatusText(scope.row.cabinetStatus) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="networkStatus" label="网络状态" width="120">
          <template #default="scope">
            <el-tag :type="scope.row.networkStatus === 1 ? 'success' : 'danger'">
              {{ scope.row.networkStatus === 1 ? '在线' : '离线' }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="lastHeartbeat" label="最后心跳" width="180" />
        <el-table-column label="操作" width="200" fixed="right">
          <template #default="scope">
            <el-button size="small" @click="viewCabinetDetail(scope.row)">详情</el-button>
            <el-button size="small" @click="editCabinet(scope.row)">编辑</el-button>
            <el-button size="small" type="warning" 
                       @click="maintainCabinet(scope.row)">维护</el-button>
          </template>
        </el-table-column>
      </el-table>
      
      <!-- 分页 -->
      <el-pagination
        @size-change="handleSizeChange"
        @current-change="handleCurrentChange"
        :current-page="pagination.current"
        :page-sizes="[10, 20, 50]"
        :page-size="pagination.size"
        layout="total, sizes, prev, pager, next, jumper"
        :total="pagination.total">
      </el-pagination>
    </el-card>

    <!-- 球杆管理 -->
    <el-card class="cue-management-card">
      <template #header>
        <span>球杆管理</span>
      </template>
      
      <el-table :data="cueList" v-loading="cueLoading">
        <el-table-column prop="cueSn" label="球杆编号" width="150" />
        <el-table-column prop="cueBrand" label="品牌" width="120" />
        <el-table-column prop="cueModel" label="型号" width="120" />
        <el-table-column prop="currentStatus" label="当前状态" width="120">
          <template #default="scope">
            <el-tag :type="getCueStatusTagType(scope.row.currentStatus)">
              {{ getCueStatusText(scope.row.currentStatus) }}
            </el-tag>
          </template>
        </el-table-column>
        <el-table-column prop="cabinetName" label="所在柜子" width="150" />
        <el-table-column prop="rentalCount" label="租赁次数" width="100" />
        <el-table-column prop="lastMaintenanceDate" label="最后维护" width="120" />
        <el-table-column prop="batteryLevel" label="电量" width="100">
          <template #default="scope">
            <el-progress v-if="scope.row.batteryLevel" 
                         :percentage="scope.row.batteryLevel" 
                         :show-text="false"></el-progress>
            <span v-else>-</span>
          </template>
        </el-table-column>
        <el-table-column label="操作" width="150">
          <template #default="scope">
            <el-button size="small" @click="maintainCue(scope.row)">维护</el-button>
            <el-button size="small" type="danger" 
                       @click="retireCue(scope.row)">退役</el-button>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
  </div>
</template>

<script>
import { getCabinets, getDashboardStats, getCues } from '@/api/cueManagement'

export default {
  name: 'CueManagement',
  data() {
    return {
      stats: {
        totalCabinets: 0,
        totalCues: 0,
        todayRentals: 0,
        todayRevenue: 0
      },
      cabinetList: [],
      cueList: [],
      loading: false,
      cueLoading: false,
      queryParams: {
        cabinetStatus: '',
        networkStatus: ''
      },
      pagination: {
        current: 1,
        size: 10,
        total: 0
      }
    }
  },
  
  mounted() {
    this.loadDashboardData()
    this.loadCabinetList()
    this.loadCueList()
  },
  
  methods: {
    async loadDashboardData() {
      try {
        const res = await getDashboardStats()
        if (res.code === 200) {
          this.stats = res.data
        }
      } catch (error) {
        this.$message.error('加载数据失败')
      }
    },
    
    async loadCabinetList() {
      this.loading = true
      try {
        const params = {
          ...this.queryParams,
          page: this.pagination.current,
          size: this.pagination.size
        }
        const res = await getCabinets(params)
        if (res.code === 200) {
          this.cabinetList = res.data.records
          this.pagination.total = res.data.total
        }
      } catch (error) {
        this.$message.error('加载柜子列表失败')
      } finally {
        this.loading = false
      }
    },
    
    async loadCueList() {
      this.cueLoading = true
      try {
        const res = await getCues()
        if (res.code === 200) {
          this.cueList = res.data
        }
      } catch (error) {
        this.$message.error('加载球杆列表失败')
      } finally {
        this.cueLoading = false
      }
    },
    
    getCabinetStatusTagType(status) {
      const map = {
        1: 'success',  // 正常
        2: 'danger',   // 离线
        3: 'warning'   // 维护中
      }
      return map[status] || 'info'
    },
    
    getCabinetStatusText(status) {
      const map = {
        1: '正常',
        2: '离线',
        3: '维护中'
      }
      return map[status] || '未知'
    },
    
    getCueStatusTagType(status) {
      const map = {
        0: 'success',  // 空闲
        1: 'primary',  // 租赁中
        2: 'warning'   // 维护中
      }
      return map[status] || 'info'
    },
    
    getCueStatusText(status) {
      const map = {
        0: '空闲',
        1: '租赁中',
        2: '维护中'
      }
      return map[status] || '未知'
    },
    
    handleSearch() {
      this.pagination.current = 1
      this.loadCabinetList()
    },
    
    handleReset() {
      this.queryParams = {
        cabinetStatus: '',
        networkStatus: ''
      }
      this.handleSearch()
    },
    
    handleSizeChange(size) {
      this.pagination.size = size
      this.loadCabinetList()
    },
    
    handleCurrentChange(current) {
      this.pagination.current = current
      this.loadCabinetList()
    },
    
    addCabinet() {
      this.$router.push('/cabinet/add')
    },
    
    viewCabinetDetail(cabinet) {
      this.$router.push(`/cabinet/detail/${cabinet.id}`)
    },
    
    editCabinet(cabinet) {
      this.$router.push(`/cabinet/edit/${cabinet.id}`)
    }
  }
}
</script>

<style scoped>
.cue-management-container {
  padding: 20px;
  background: #f5f7fa;
}

.stats-card {
  text-align: center;
}

.stats-number {
  font-size: 32px;
  font-weight: bold;
  color: #1890ff;
  margin-bottom: 8px;
}

.stats-label {
  color: #666;
  font-size: 14px;
}

.table-card {
  margin-top: 20px;
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.cue-management-card {
  margin-top: 20px;
}
</style>
数据库设计核心表结构
复制代码
-- 智能柜表
CREATE TABLE `smart_cabinet` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `cabinet_sn` varchar(50) NOT NULL COMMENT '柜子序列号',
  `cabinet_name` varchar(100) NOT NULL COMMENT '柜子名称',
  `location` varchar(255) NOT NULL COMMENT '安装位置',
  `geo_location` varchar(100) NOT NULL COMMENT '地理坐标',
  `total_slots` int(11) NOT NULL COMMENT '总槽位数',
  `available_slots` int(11) NOT NULL COMMENT '可用槽位',
  `cabinet_status` tinyint(1) NOT NULL COMMENT '柜子状态',
  `network_status` tinyint(1) NOT NULL COMMENT '网络状态',
  `power_status` tinyint(1) NOT NULL COMMENT '电源状态',
  `last_heartbeat` datetime DEFAULT NULL COMMENT '最后心跳时间',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_cabinet_sn` (`cabinet_sn`),
  KEY `idx_location` (`geo_location`),
  KEY `idx_status` (`cabinet_status`),
  KEY `idx_heartbeat` (`last_heartbeat`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='智能柜表';

-- 台球杆表
CREATE TABLE `billiards_cue` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `cue_sn` varchar(50) NOT NULL COMMENT '球杆唯一编号',
  `cue_brand` varchar(50) NOT NULL COMMENT '球杆品牌',
  `cue_model` varchar(50) NOT NULL COMMENT '球杆型号',
  `cue_weight` decimal(5,2) NOT NULL COMMENT '球杆重量',
  `cue_tip_size` decimal(3,1) NOT NULL COMMENT '杆头尺寸',
  `current_status` tinyint(1) NOT NULL COMMENT '当前状态',
  `cabinet_id` bigint(20) DEFAULT NULL COMMENT '所属柜子ID',
  `slot_position` int(11) DEFAULT NULL COMMENT '槽位位置',
  `rental_count` int(11) DEFAULT '0' COMMENT '租赁次数',
  `last_maintenance_date` date DEFAULT NULL COMMENT '最后维护日期',
  `battery_level` int(11) DEFAULT NULL COMMENT '电量',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_cue_sn` (`cue_sn`),
  KEY `idx_cabinet` (`cabinet_id`),
  KEY `idx_status` (`current_status`),
  KEY `idx_maintenance` (`last_maintenance_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='台球杆表';

-- 租赁订单表
CREATE TABLE `cue_rental_order` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `order_no` varchar(32) NOT NULL COMMENT '订单编号',
  `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  `cue_id` bigint(20) NOT NULL COMMENT '球杆ID',
  `cabinet_id` bigint(20) NOT NULL COMMENT '柜子ID',
  `rental_start_time` datetime NOT NULL COMMENT '租赁开始时间',
  `rental_end_time` datetime DEFAULT NULL COMMENT '租赁结束时间',
  `actual_return_time` datetime DEFAULT NULL COMMENT '实际归还时间',
  `rental_duration` int(11) DEFAULT NULL COMMENT '租赁时长',
  `total_amount` decimal(10,2) DEFAULT NULL COMMENT '订单总额',
  `payment_status` tinyint(1) NOT NULL COMMENT '支付状态',
  `order_status` tinyint(1) NOT NULL COMMENT '订单状态',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_cue_id` (`cue_id`),
  KEY `idx_cabinet_id` (`cabinet_id`),
  KEY `idx_rental_time` (`rental_start_time`),
  KEY `idx_order_status` (`order_status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='租赁订单表';

-- 设备维护记录表
CREATE TABLE `device_maintenance` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `device_type` tinyint(1) NOT NULL COMMENT '设备类型:1-柜子 2-球杆',
  `device_id` bigint(20) NOT NULL COMMENT '设备ID',
  `maintenance_type` tinyint(1) NOT NULL COMMENT '维护类型',
  `maintenance_desc` text COMMENT '维护描述',
  `maintenance_date` date NOT NULL COMMENT '维护日期',
  `maintenance_person` varchar(50) NOT NULL COMMENT '维护人员',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_device` (`device_type`, `device_id`),
  KEY `idx_maintenance_date` (`maintenance_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备维护记录表';

这套基于JAVA无人共享台球杆台球柜系统 的完整解决方案,通过现代化的物联网技术和智能管理平台,为体育器材共享行业提供了全方位的数字化转型升级路径。系统采用SpringBoot+MyBatisPlus+MySQL 构建高可用分布式服务集群,使用Uniapp 框架实现微信小程序 客户端,配合Vue+ElementUI的管理后台,形成了完整的技术生态闭环。

从技术实现角度看,该系统创新性地引入了智能调度算法、设备状态监控、预防性维护机制等先进技术理念,确保了共享租赁系统在复杂使用场景下的稳定性和用户体验。对于体育器材共享运营商而言,这套系统不仅实现了租赁业务的数字化管理,更重要的是通过数据分析和智能算法为设备优化配置和维护策略提供了科学依据。

随着体育产业数字化进程的加速和共享经济模式的深化,智能体育器材共享正在成为新的市场增长点。这套JAVA无人共享台球杆台球柜系统源码以其先进的技术架构、完善的功能设计和良好的可扩展性,将成为体育器材共享行业标准化发展的重要技术支撑。未来,随着5G、人工智能等新技术的深度融合,该系统还将持续演进,为体育器材共享行业的创新发展提供更强大的技术动力。

相关推荐
Fortunate Chen4 小时前
初识C语言13.自定义类型(联合体与枚举)
c语言·开发语言
麦麦鸡腿堡4 小时前
Java的抽象类实践-模板设计模式
java·开发语言·设计模式
沙虫一号4 小时前
聊聊Java里的那把锁:ReentrantLock到底有多强大?
java
无心水5 小时前
Java主流锁全解析:从分类到实践
java·面试·架构
云知谷5 小时前
【经典书籍】《编写可读代码的艺术》精华
开发语言·c++·软件工程·团队开发
空空kkk5 小时前
Java——接口
java·开发语言·python
MaxHua5 小时前
JAVA开发处理金额的数据类型你知道多少?
java·后端
oak隔壁找我5 小时前
公司级 Maven Parent POM 设计指南
java·后端
zl9798995 小时前
SpringBoot-Web开发之内容协商
java·spring boot