博客 / 詳情

返回

全開源上門服務+預約家政小程序源碼快速搭建保姆級教程

本教程將詳細介紹如何使用開源家政小程序源碼快速搭建一個功能完整的上門服務預約平台。該系統採用前後端分離架構,包含微信小程序前端、Spring Boot後端管理系統,支持服務預約、在線支付、服務人員管理、多區域服務等核心功能。

技術選型

  1. 前端技術棧

    • 源碼及演示:j.yunzes.top/er
    • 框架:微信小程序原生框架(WXML、WXSS、JavaScript),支持快速構建美觀、響應迅速的用户界面,且即用即走,降低使用門檻。也可選擇Uni-app、Taro等跨平台框架,實現一套代碼多端運行。
    • UI組件庫:Vant Weapp、WeUI等,提供豐富的組件,加速開發進程。
  2. 後端技術棧

    • 框架:Node.js(Express/Koa)或Java(Spring Boot),Node.js適合高併發I/O場景,Spring Boot生態成熟穩定。
    • 數據庫:MySQL(關係型,存儲用户、訂單等核心數據)+ MongoDB(非關係型,存儲評價、日誌等半結構化數據)。
    • 緩存:Redis,用於緩存熱點數據、會話管理,提升系統性能。
  3. 基礎設施

    • 容器化:Docker,實現環境標準化,便於部署與擴展。
    • 反向代理:Nginx,提升首屏加載速度,處理靜態資源。

環境要求

  • Node.js 16+
  • JDK 11+
  • MySQL 8.0+
  • Redis 6+
  • 微信開發者工具

項目結構説明

前端項目結構

weapp-homemaking/
├── miniprogram/           # 小程序主包
│   ├── pages/            # 頁面文件
│   │   ├── index/        # 首頁
│   │   ├── service/      # 服務列表
│   │   ├── order/        # 訂單相關
│   │   └── user/         # 個人中心
│   ├── components/       # 自定義組件
│   ├── utils/           # 工具函數
│   ├── config/          # 配置文件
│   └── app.js           # 小程序入口
└── project.config.json  # 項目配置

2.2 後端項目結構

homemaking-server/
├── src/main/java/com/homemaking/
│   ├── controller/      # 控制器層
│   ├── service/         # 服務層
│   ├── mapper/          # 數據訪問層
│   ├── entity/          # 實體類
│   ├── dto/            # 數據傳輸對象
│   ├── vo/             # 視圖對象
│   └── config/         # 配置類
├── resources/
│   ├── application.yml  # 主配置文件
│   └── mapper/         # MyBatis XML
└── pom.xml            # Maven配置

數據庫初始化

創建數據庫

-- 創建數據庫
CREATE DATABASE IF NOT EXISTS `homemaking` 
DEFAULT CHARACTER SET utf8mb4 
COLLATE utf8mb4_unicode_ci;

USE `homemaking`;

-- 服務分類表
CREATE TABLE `service_category` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL COMMENT '分類名稱',
  `icon` varchar(255) DEFAULT NULL COMMENT '圖標',
  `parent_id` int(11) DEFAULT 0 COMMENT '父級ID',
  `sort` int(11) DEFAULT 0 COMMENT '排序',
  `status` tinyint(1) DEFAULT 1 COMMENT '狀態:1啓用 0禁用',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 服務項目表
CREATE TABLE `service_item` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `category_id` int(11) NOT NULL COMMENT '分類ID',
  `title` varchar(100) NOT NULL COMMENT '服務標題',
  `cover_image` varchar(255) DEFAULT NULL COMMENT '封面圖',
  `description` text COMMENT '服務描述',
  `price` decimal(10,2) NOT NULL COMMENT '價格',
  `unit` varchar(20) DEFAULT '小時' COMMENT '單位',
  `duration` int(11) DEFAULT 60 COMMENT '服務時長(分鐘)',
  `status` tinyint(1) DEFAULT 1 COMMENT '狀態',
  `sales_count` int(11) DEFAULT 0 COMMENT '銷量',
  `is_recommend` tinyint(1) DEFAULT 0 COMMENT '是否推薦',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_category` (`category_id`),
  KEY `idx_recommend` (`is_recommend`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 服務區域表
CREATE TABLE `service_region` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL COMMENT '區域名稱',
  `code` varchar(20) DEFAULT NULL COMMENT '區域編碼',
  `parent_code` varchar(20) DEFAULT NULL COMMENT '父級編碼',
  `level` tinyint(1) DEFAULT 1 COMMENT '層級 1:省 2:市 3:區',
  `is_enabled` tinyint(1) DEFAULT 1 COMMENT '是否啓用服務',
  `extra_fee` decimal(10,2) DEFAULT 0.00 COMMENT '附加費用',
  `longitude` decimal(10,6) DEFAULT NULL COMMENT '經度',
  `latitude` decimal(10,6) DEFAULT NULL COMMENT '緯度',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_code` (`code`),
  KEY `idx_parent` (`parent_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 用户表
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `openid` varchar(100) NOT NULL COMMENT '微信openid',
  `unionid` varchar(100) DEFAULT NULL,
  `nickname` varchar(100) DEFAULT NULL COMMENT '暱稱',
  `avatar` varchar(500) DEFAULT NULL COMMENT '頭像',
  `phone` varchar(20) DEFAULT NULL COMMENT '手機號',
  `gender` tinyint(1) DEFAULT 0 COMMENT '性別 0:未知 1:男 2:女',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `last_login_time` datetime DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_openid` (`openid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 訂單表
CREATE TABLE `service_order` (
  `id` varchar(32) NOT NULL COMMENT '訂單號',
  `user_id` int(11) NOT NULL COMMENT '用户ID',
  `service_item_id` int(11) NOT NULL COMMENT '服務項目ID',
  `service_date` date NOT NULL COMMENT '服務日期',
  `service_time` varchar(20) NOT NULL COMMENT '服務時間',
  `address` varchar(500) NOT NULL COMMENT '服務地址',
  `contact_name` varchar(50) NOT NULL COMMENT '聯繫人',
  `contact_phone` varchar(20) NOT NULL COMMENT '聯繫電話',
  `region_code` varchar(20) NOT NULL COMMENT '區域編碼',
  `quantity` int(11) DEFAULT 1 COMMENT '數量',
  `unit_price` decimal(10,2) NOT NULL COMMENT '單價',
  `total_amount` decimal(10,2) NOT NULL COMMENT '總金額',
  `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '0:待支付 1:已支付 2:已接單 3:服務中 4:已完成 5:已取消',
  `pay_type` varchar(20) DEFAULT NULL COMMENT '支付方式',
  `pay_time` datetime DEFAULT NULL COMMENT '支付時間',
  `worker_id` int(11) DEFAULT NULL COMMENT '服務人員ID',
  `cancel_reason` varchar(200) DEFAULT NULL COMMENT '取消原因',
  `remark` varchar(500) DEFAULT NULL COMMENT '備註',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_status` (`status`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 服務人員表
CREATE TABLE `service_worker` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) NOT NULL COMMENT '姓名',
  `avatar` varchar(500) DEFAULT NULL COMMENT '頭像',
  `phone` varchar(20) NOT NULL COMMENT '手機號',
  `id_card` varchar(20) DEFAULT NULL COMMENT '身份證',
  `service_category_ids` varchar(500) DEFAULT NULL COMMENT '可服務分類',
  `region_codes` varchar(1000) DEFAULT NULL COMMENT '可服務區域',
  `rating` decimal(3,2) DEFAULT 5.00 COMMENT '評分',
  `order_count` int(11) DEFAULT 0 COMMENT '接單數',
  `status` tinyint(1) DEFAULT 1 COMMENT '狀態 1:在線 0:離線',
  `create_time` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `idx_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

-- 插入初始數據
INSERT INTO `service_category` (`name`, `icon`, `parent_id`, `sort`) VALUES
('保潔服務', '/images/icons/clean.png', 0, 1),
('深度保潔', '/images/icons/deep-clean.png', 1, 1),
('日常保潔', '/images/icons/daily-clean.png', 1, 2),
('家電清洗', '/images/icons/appliance.png', 0, 2),
('空調清洗', '/images/icons/ac.png', 4, 1),
('油煙機清洗', '/images/icons/hood.png', 4, 2);

INSERT INTO `service_item` (`category_id`, `title`, `price`, `unit`, `duration`, `description`) VALUES
(2, '4小時深度全屋保潔', 199.00, '次', 240, '專業團隊,深度清潔,包含廚房、衞生間等重點區域'),
(3, '3小時日常保潔', 99.00, '次', 180, '日常清潔維護,保持家居整潔'),
(5, '掛式空調深度清洗', 129.00, '台', 60, '拆機清洗,高温蒸汽消毒');

後端服務搭建

Spring Boot項目初始化

// pom.xml 關鍵依賴
<dependencies>
    <!-- Spring Boot Starter -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    <!-- 數據庫相關 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.3</version>
    </dependency>
    
    <!-- 工具類 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>2.0.34</version>
    </dependency>
    
    <!-- 微信支付 -->
    <dependency>
        <groupId>com.github.wechatpay-apiv3</groupId>
        <artifactId>wechatpay-java</artifactId>
        <version>0.2.14</version>
    </dependency>
</dependencies>

應用配置文件

# application.yml
spring:
  application:
    name: homemaking-server
  
  # 數據源配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/homemaking?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai
    username: root
    password: 123456
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
  
  # Redis配置
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0
    lettuce:
      pool:
        max-active: 20
        max-idle: 10
        min-idle: 5
  
  # 文件上傳
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 20MB

# MyBatis Plus配置
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.homemaking.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  global-config:
    db-config:
      logic-delete-field: deleted
      logic-delete-value: 1
      logic-not-delete-value: 0

# 微信小程序配置
wechat:
  appid: wx你的appid
  secret: 你的appsecret
  mch-id: 商户號
  api-v3-key: APIv3密鑰
  private-key-path: classpath:apiclient_key.pem
  
# 服務配置
server:
  port: 8080
  servlet:
    context-path: /api

核心業務代碼實現

用户登錄控制器

// UserController.java
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    
    @Autowired
    private UserService userService;
    
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    
    /**
     * 微信小程序登錄
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginDTO loginDTO) {
        try {
            // 1. 調用微信接口獲取openid
            Map<String, String> wechatSession = wechatService.code2Session(loginDTO.getCode());
            String openid = wechatSession.get("openid");
            String sessionKey = wechatSession.get("session_key");
            
            if (StringUtils.isEmpty(openid)) {
                return Result.error("微信登錄失敗");
            }
            
            // 2. 查詢或創建用户
            User user = userService.getOrCreateUser(openid, loginDTO.getUserInfo());
            
            // 3. 生成JWT Token
            String token = JwtUtil.generateToken(user.getId(), openid);
            
            // 4. 保存session到Redis
            String redisKey = "user:session:" + user.getId();
            redisTemplate.opsForValue().set(redisKey, sessionKey, 7, TimeUnit.DAYS);
            
            // 5. 更新最後登錄時間
            userService.updateLastLoginTime(user.getId());
            
            Map<String, Object> data = new HashMap<>();
            data.put("token", token);
            data.put("userInfo", user);
            
            return Result.success(data);
            
        } catch (Exception e) {
            log.error("登錄失敗", e);
            return Result.error("登錄失敗:" + e.getMessage());
        }
    }
    
    /**
     * 更新用户手機號
     */
    @PostMapping("/updatePhone")
    public Result updatePhone(@RequestBody UpdatePhoneDTO dto, 
                            HttpServletRequest request) {
        Long userId = JwtUtil.getUserId(request);
        
        // 解密手機號
        String phone = wechatService.decryptPhone(
            dto.getEncryptedData(), 
            dto.getIv(), 
            userId
        );
        
        if (StringUtils.isEmpty(phone)) {
            return Result.error("解密手機號失敗");
        }
        
        userService.updatePhone(userId, phone);
        return Result.success();
    }
}

服務預約控制器

// OrderController.java
@RestController
@RequestMapping("/order")
@Slf4j
public class OrderController {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private WechatPayService wechatPayService;
    
    /**
     * 創建服務預約訂單
     */
    @PostMapping("/create")
    public Result createOrder(@RequestBody @Valid CreateOrderDTO dto,
                            HttpServletRequest request) {
        Long userId = JwtUtil.getUserId(request);
        
        try {
            // 1. 驗證服務項目和庫存
            ServiceItem serviceItem = orderService.validateServiceItem(dto.getServiceItemId());
            
            // 2. 驗證預約時間是否可用
            orderService.validateServiceTime(dto.getServiceDate(), dto.getServiceTime());
            
            // 3. 驗證服務區域
            orderService.validateServiceRegion(dto.getRegionCode());
            
            // 4. 創建訂單
            String orderId = orderService.createOrder(dto, userId, serviceItem);
            
            // 5. 返回預支付信息
            Map<String, Object> prepayInfo = wechatPayService.createPrepay(orderId, 
                dto.getTotalAmount(), "家政服務預約");
            
            Map<String, Object> data = new HashMap<>();
            data.put("orderId", orderId);
            data.put("prepayInfo", prepayInfo);
            
            return Result.success(data);
            
        } catch (BusinessException e) {
            return Result.error(e.getMessage());
        } catch (Exception e) {
            log.error("創建訂單失敗", e);
            return Result.error("系統繁忙,請稍後重試");
        }
    }
    
    /**
     * 獲取用户訂單列表
     */
    @GetMapping("/list")
    public Result getOrderList(@RequestParam(defaultValue = "0") Integer status,
                             @RequestParam(defaultValue = "1") Integer page,
                             @RequestParam(defaultValue = "10") Integer size,
                             HttpServletRequest request) {
        Long userId = JwtUtil.getUserId(request);
        
        Page<OrderVO> orderPage = orderService.getUserOrderList(userId, status, page, size);
        return Result.success(orderPage);
    }
    
    /**
     * 取消訂單
     */
    @PostMapping("/cancel/{orderId}")
    public Result cancelOrder(@PathVariable String orderId,
                            @RequestParam(required = false) String reason,
                            HttpServletRequest request) {
        Long userId = JwtUtil.getUserId(request);
        
        orderService.cancelOrder(orderId, userId, reason);
        return Result.success();
    }
}

服務項目管理

// ServiceItemServiceImpl.java
@Service
@Slf4j
public class ServiceItemServiceImpl extends ServiceImpl<ServiceItemMapper, ServiceItem> 
    implements ServiceItemService {
    
    @Autowired
    private ServiceCategoryMapper categoryMapper;
    
    @Autowired
    private ServiceRegionMapper regionMapper;
    
    @Override
    public Page<ServiceItemVO> getServiceList(Long categoryId, String keyword, 
                                             String regionCode, Integer page, Integer size) {
        Page<ServiceItem> pageParam = new Page<>(page, size);
        
        // 構建查詢條件
        LambdaQueryWrapper<ServiceItem> query = new LambdaQueryWrapper<>();
        query.eq(ServiceItem::getStatus, 1);
        
        if (categoryId != null) {
            // 查詢該分類及其子分類的所有服務
            List<Long> categoryIds = getSubCategoryIds(categoryId);
            if (!categoryIds.isEmpty()) {
                query.in(ServiceItem::getCategoryId, categoryIds);
            }
        }
        
        if (StringUtils.isNotBlank(keyword)) {
            query.and(wrapper -> wrapper
                .like(ServiceItem::getTitle, keyword)
                .or()
                .like(ServiceItem::getDescription, keyword)
            );
        }
        
        // 按推薦和銷量排序
        query.orderByDesc(ServiceItem::getIsRecommend);
        query.orderByDesc(ServiceItem::getSalesCount());
        
        // 執行查詢
        Page<ServiceItem> itemPage = this.page(pageParam, query);
        
        // 轉換為VO
        return itemPage.convert(item -> {
            ServiceItemVO vo = new ServiceItemVO();
            BeanUtils.copyProperties(item, vo);
            
            // 查詢分類信息
            ServiceCategory category = categoryMapper.selectById(item.getCategoryId());
            if (category != null) {
                vo.setCategoryName(category.getName());
            }
            
            // 計算區域附加費
            if (StringUtils.isNotBlank(regionCode)) {
                BigDecimal extraFee = calculateRegionExtraFee(regionCode, item.getPrice());
                vo.setExtraFee(extraFee);
                vo.setFinalPrice(item.getPrice().add(extraFee));
            } else {
                vo.setFinalPrice(item.getPrice());
            }
            
            return vo;
        });
    }
    
    /**
     * 獲取分類及其所有子分類ID
     */
    private List<Long> getSubCategoryIds(Long categoryId) {
        List<Long> categoryIds = new ArrayList<>();
        categoryIds.add(categoryId);
        
        // 遞歸查詢子分類
        getChildCategoryIds(categoryId, categoryIds);
        return categoryIds;
    }
    
    private void getChildCategoryIds(Long parentId, List<Long> result) {
        List<ServiceCategory> children = categoryMapper.selectList(
            new LambdaQueryWrapper<ServiceCategory>()
                .eq(ServiceCategory::getParentId, parentId)
                .eq(ServiceCategory::getStatus, 1)
        );
        
        for (ServiceCategory child : children) {
            result.add(child.getId());
            getChildCategoryIds(child.getId(), result);
        }
    }
    
    /**
     * 計算區域附加費
     */
    private BigDecimal calculateRegionExtraFee(String regionCode, BigDecimal basePrice) {
        ServiceRegion region = regionMapper.selectByCode(regionCode);
        if (region == null || !region.getIsEnabled()) {
            throw new BusinessException("該區域暫不提供服務");
        }
        
        // 如果設置了附加費百分比
        if (region.getExtraFeeRate() != null && region.getExtraFeeRate().compareTo(BigDecimal.ZERO) > 0) {
            return basePrice.multiply(region.getExtraFeeRate());
        }
        
        return region.getExtraFee() != null ? region.getExtraFee() : BigDecimal.ZERO;
    }
}

微信小程序前端開發

項目初始化

// app.json
{
  "pages": [
    "pages/index/index",
    "pages/service/list/list",
    "pages/service/detail/detail",
    "pages/order/create/create",
    "pages/order/list/list",
    "pages/order/detail/detail",
    "pages/user/user"
  ],
  "window": {
    "navigationBarTitleText": "家政服務",
    "navigationBarBackgroundColor": "#07c160",
    "navigationBarTextStyle": "white"
  },
  "tabBar": {
    "color": "#999999",
    "selectedColor": "#07c160",
    "list": [
      {
        "pagePath": "pages/index/index",
        "text": "首頁",
        "iconPath": "images/tabbar/home.png",
        "selectedIconPath": "images/tabbar/home-active.png"
      },
      {
        "pagePath": "pages/service/list/list",
        "text": "服務",
        "iconPath": "images/tabbar/service.png",
        "selectedIconPath": "images/tabbar/service-active.png"
      },
      {
        "pagePath": "pages/user/user",
        "text": "我的",
        "iconPath": "images/tabbar/user.png",
        "selectedIconPath": "images/tabbar/user-active.png"
      }
    ]
  },
  "permission": {
    "scope.userLocation": {
      "desc": "您的位置信息將用於確定服務區域"
    }
  }
}

首頁實現

// pages/index/index.js
Page({
  data: {
    banners: [
      { id: 1, image: '/images/banner/1.jpg', link: '' },
      { id: 2, image: '/images/banner/2.jpg', link: '' }
    ],
    categories: [],
    recommendServices: [],
    location: '定位中...',
    userInfo: null
  },

  onLoad() {
    this.loadData();
    this.getLocation();
  },

  onShow() {
    this.checkLogin();
  },

  // 獲取定位
  getLocation() {
    wx.getLocation({
      type: 'wgs84',
      success: (res) => {
        this.reverseGeocoder(res.latitude, res.longitude);
      },
      fail: () => {
        this.setData({ location: '獲取定位失敗' });
      }
    });
  },

  // 逆地理編碼
  reverseGeocoder(latitude, longitude) {
    const qqmapKey = '您申請的騰訊地圖key';
    
    wx.request({
      url: 'https://apis.map.qq.com/ws/geocoder/v1/',
      data: {
        location: `${latitude},${longitude}`,
        key: qqmapKey,
        get_poi: 0
      },
      success: (res) => {
        if (res.data.status === 0) {
          const address = res.data.result.address_component;
          this.setData({
            location: address.district || address.city
          });
        }
      }
    });
  },

  // 加載首頁數據
  loadData() {
    this.loadCategories();
    this.loadRecommendServices();
  },

  // 加載服務分類
  loadCategories() {
    wx.request({
      url: `${app.globalData.baseUrl}/api/category/list`,
      data: { parentId: 0 },
      success: (res) => {
        if (res.data.code === 200) {
          this.setData({ categories: res.data.data });
        }
      }
    });
  },

  // 加載推薦服務
  loadRecommendServices() {
    wx.request({
      url: `${app.globalData.baseUrl}/api/service/recommend`,
      data: { limit: 6 },
      success: (res) => {
        if (res.data.code === 200) {
          this.setData({ recommendServices: res.data.data });
        }
      }
    });
  },

  // 檢查登錄狀態
  checkLogin() {
    const token = wx.getStorageSync('token');
    if (token) {
      this.getUserInfo();
    }
  },

  // 獲取用户信息
  getUserInfo() {
    wx.request({
      url: `${app.globalData.baseUrl}/api/user/info`,
      header: { 'Authorization': `Bearer ${wx.getStorageSync('token')}` },
      success: (res) => {
        if (res.data.code === 200) {
          this.setData({ userInfo: res.data.data });
        }
      }
    });
  },

  // 跳轉到服務列表
  goToServiceList(e) {
    const categoryId = e.currentTarget.dataset.id;
    wx.navigateTo({
      url: `/pages/service/list/list?categoryId=${categoryId}`
    });
  },

  // 跳轉到服務詳情
  goToServiceDetail(e) {
    const serviceId = e.currentTarget.dataset.id;
    wx.navigateTo({
      url: `/pages/service/detail/detail?id=${serviceId}`
    });
  },

  // 用户登錄
  handleLogin() {
    wx.getUserProfile({
      desc: '用於完善會員資料',
      success: (res) => {
        wx.login({
          success: (loginRes) => {
            wx.request({
              url: `${app.globalData.baseUrl}/api/user/login`,
              method: 'POST',
              data: {
                code: loginRes.code,
                userInfo: res.userInfo
              },
              success: (apiRes) => {
                if (apiRes.data.code === 200) {
                  const { token, userInfo } = apiRes.data.data;
                  wx.setStorageSync('token', token);
                  this.setData({ userInfo });
                  wx.showToast({ title: '登錄成功' });
                }
              }
            });
          }
        });
      }
    });
  }
});

服務預約頁面

// pages/order/create/create.js
Page({
  data: {
    serviceId: null,
    serviceDetail: null,
    selectedDate: '',
    selectedTime: '',
    timeSlots: [],
    address: '',
    contactName: '',
    contactPhone: '',
    quantity: 1,
    regionCode: '',
    totalAmount: 0,
    isSubmitting: false
  },

  onLoad(options) {
    this.setData({ serviceId: options.id });
    this.loadServiceDetail();
    this.initDefaultData();
    this.generateTimeSlots();
  },

  // 加載服務詳情
  loadServiceDetail() {
    wx.showLoading({ title: '加載中...' });
    
    wx.request({
      url: `${app.globalData.baseUrl}/api/service/detail/${this.data.serviceId}`,
      data: { regionCode: this.data.regionCode },
      success: (res) => {
        if (res.data.code === 200) {
          this.setData({ 
            serviceDetail: res.data.data,
            totalAmount: res.data.data.finalPrice
          });
        }
      },
      complete: wx.hideLoading
    });
  },

  // 初始化默認數據
  initDefaultData() {
    const today = new Date();
    const dateStr = today.toISOString().split('T')[0];
    
    // 獲取默認地址
    const address = wx.getStorageSync('defaultAddress') || '';
    
    // 獲取用户信息
    const userInfo = wx.getStorageSync('userInfo') || {};
    
    this.setData({
      selectedDate: dateStr,
      address: address,
      contactName: userInfo.nickname || '',
      contactPhone: userInfo.phone || ''
    });
  },

  // 生成可選時間段
  generateTimeSlots() {
    const slots = [];
    const today = new Date();
    const currentHour = today.getHours();
    
    // 生成未來7天的時間段
    for (let day = 1; day <= 7; day++) {
      const date = new Date();
      date.setDate(today.getDate() + day);
      const dateStr = date.toISOString().split('T')[0];
      
      // 每天8:00-20:00,每2小時一個時間段
      for (let hour = 8; hour <= 20; hour += 2) {
        // 如果是今天,跳過已過去的時間
        if (day === 1 && hour <= currentHour) {
          continue;
        }
        
        slots.push({
          date: dateStr,
          time: `${hour}:00-${hour + 2}:00`,
          display: `${dateStr} ${hour}:00`
        });
      }
    }
    
    this.setData({ timeSlots: slots });
    
    // 設置默認時間
    if (slots.length > 0) {
      this.setData({ selectedTime: slots[0].time });
    }
  },

  // 選擇日期
  onDateChange(e) {
    this.setData({ selectedDate: e.detail.value });
  },

  // 選擇時間
  onTimeChange(e) {
    this.setData({ selectedTime: e.detail.value });
  },

  // 選擇地址
  chooseAddress() {
    wx.chooseLocation({
      success: (res) => {
        this.setData({
          address: res.address + res.name
        });
        wx.setStorageSync('defaultAddress', res.address + res.name);
      }
    });
  },

  // 數量變化
  onQuantityChange(e) {
    const quantity = e.detail;
    const { serviceDetail } = this.data;
    
    this.setData({
      quantity,
      totalAmount: serviceDetail.finalPrice * quantity
    });
  },

  // 表單驗證
  validateForm() {
    const { address, contactName, contactPhone, selectedTime } = this.data;
    
    if (!address) {
      wx.showToast({ title: '請選擇服務地址', icon: 'none' });
      return false;
    }
    
    if (!contactName) {
      wx.showToast({ title: '請輸入聯繫人姓名', icon: 'none' });
      return false;
    }
    
    if (!contactPhone || !/^1[3-9]\d{9}$/.test(contactPhone)) {
      wx.showToast({ title: '請輸入正確的手機號', icon: 'none' });
      return false;
    }
    
    if (!selectedTime) {
      wx.showToast({ title: '請選擇服務時間', icon: 'none' });
      return false;
    }
    
    return true;
  },

  // 提交預約
  submitOrder() {
    if (!this.validateForm()) {
      return;
    }
    
    if (this.data.isSubmitting) {
      return;
    }
    
    this.setData({ isSubmitting: true });
    
    const orderData = {
      serviceItemId: this.data.serviceId,
      serviceDate: this.data.selectedDate,
      serviceTime: this.data.selectedTime,
      address: this.data.address,
      contactName: this.data.contactName,
      contactPhone: this.data.contactPhone,
      regionCode: this.data.regionCode,
      quantity: this.data.quantity,
      totalAmount: this.data.totalAmount
    };
    
    wx.request({
      url: `${app.globalData.baseUrl}/api/order/create`,
      method: 'POST',
      header: {
        'Authorization': `Bearer ${wx.getStorageSync('token')}`,
        'Content-Type': 'application/json'
      },
      data: orderData,
      success: (res) => {
        if (res.data.code === 200) {
          this.requestPayment(res.data.data);
        } else {
          wx.showToast({ title: res.data.message, icon: 'none' });
          this.setData({ isSubmitting: false });
        }
      },
      fail: () => {
        wx.showToast({ title: '網絡錯誤,請重試', icon: 'none' });
        this.setData({ isSubmitting: false });
      }
    });
  },

  // 調起微信支付
  requestPayment(orderData) {
    const { prepayInfo } = orderData;
    
    wx.requestPayment({
      timeStamp: prepayInfo.timeStamp,
      nonceStr: prepayInfo.nonceStr,
      package: prepayInfo.package,
      signType: prepayInfo.signType,
      paySign: prepayInfo.paySign,
      success: () => {
        wx.redirectTo({
          url: `/pages/order/detail/detail?id=${orderData.orderId}`
        });
      },
      fail: (err) => {
        wx.showToast({ title: '支付失敗', icon: 'none' });
        this.setData({ isSubmitting: false });
      }
    });
  }
});

部署上線

後端部署

# 1. 打包項目
cd homemaking-server
mvn clean package -DskipTests

# 2. 上傳jar包到服務器
scp target/homemaking-server.jar root@your-server:/home/homemaking/

# 3. 創建啓動腳本
cat > start.sh << EOF
#!/bin/bash
nohup java -jar homemaking-server.jar \
  --spring.profiles.active=prod \
  > app.log 2>&1 &
EOF

# 4. 設置執行權限並啓動
chmod +x start.sh
./start.sh

Nginx配置

# /etc/nginx/conf.d/homemaking.conf
upstream homemaking_backend {
    server 127.0.0.1:8080;
    keepalive 32;
}

server {
    listen 80;
    server_name api.yourdomain.com;
    
    # 前端靜態文件
    location / {
        root /home/homemaking/web;
        index index.html;
        try_files $uri $uri/ /index.html;
    }
    
    # 後端API代理
    location /api/ {
        proxy_pass http://homemaking_backend;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        
        # 上傳文件大小限制
        client_max_body_size 20M;
    }
    
    # 靜態資源
    location /uploads/ {
        alias /home/homemaking/uploads/;
        expires 30d;
    }
}

常見問題解決

部署常見問題

  1. 數據庫連接失敗:檢查MySQL服務狀態和賬號權限
  2. Redis連接失敗:檢查Redis配置和防火牆設置
  3. 端口被佔用:修改application.yml中的端口號
  4. 文件上傳失敗:檢查Nginx的client_max_body_size配置

開發常見問題

  1. 微信登錄失敗:檢查appid和secret是否正確
  2. 支付失敗:檢查商户號和證書配置
  3. 跨域問題:確保後端已正確配置CORS
  4. 真機調試問題:確保已配置合法域名

結語

通過本教程,您已經完成了從零開始搭建一個完整家政服務預約系統的全過程。本系統採用現代化的技術架構,具有良好的擴展性和可維護性,可以滿足大部分家政服務的業務需求。至此,我們已經完成了一站式上門家政服務小程序從零到一的全過程搭建。
技術實現上,我們採用了當前主流且成熟的Spring Boot + 微信小程序技術棧,確保了系統的穩定性和開發效率。清晰的代碼結構、完善的註釋以及詳細的部署文檔,使得無論是初創團隊快速驗證模式,還是技術開發者進行二次開發,都能夠順暢上手。從數據庫設計到接口封裝,從前端交互到服務部署,每個環節都體現了工程化的思考。
家政服務的數字化之路方興未艾,這套源碼提供了一個可靠的起點。我們鼓勵開發者在理解其設計思想的基礎上,結合具體的業務洞察,在智能調度、用户體驗、運營工具等方面進行深化和創新,從而構建出真正解決用户痛點、提升行業效率的優秀產品。技術的價值在於創造連接與改善體驗,期待您的實踐能為更多家庭帶來便捷與安心。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.