Java+vue3房屋租赁收租系统开发实践

王子源Aisling

1. 项目概述:Java房屋租赁收租系统vue3

最近在帮朋友改造他们公司的房屋租赁管理系统,用Java+vue3技术栈重构了一套收租系统。这个系统主要解决房产中介公司对多房源租金管理的痛点:手工记账易出错、催缴效率低、数据统计困难。我们团队用Spring Boot处理业务逻辑,Vue3构建动态前端界面,实现了租约管理、账单生成、在线支付和财务统计的全流程数字化。

传统Excel管理方式存在三个致命缺陷:一是租金变动时需手动修改所有关联表格,二是无法实时跟踪付款状态,三是缺乏可视化数据分析。新系统上线后,财务人员处理租金业务的效率提升了60%,逾期率下降了45%。特别在vue3的composition API加持下,复杂账单页面的渲染性能比原先jQuery方案快3倍以上。

2. 技术架构设计

2.1 后端技术选型

采用Spring Boot 2.7 + MyBatis Plus组合,考虑因素有三:

  1. MyBatis Plus的Lambda查询构建器能优雅处理多条件租金查询
  2. Spring Schedule实现每日凌晨自动生成账单
  3. 支付宝/微信支付SDK集成更成熟

数据库设计核心表包括:

sql复制CREATE TABLE `lease_contract` (
  `id` BIGINT PRIMARY KEY,
  `room_id` BIGINT NOT NULL COMMENT '房源ID',
  `tenant_id` BIGINT NOT NULL COMMENT '租客ID',
  `start_date` DATE NOT NULL COMMENT '起租日',
  `end_date` DATE NOT NULL COMMENT '到期日',
  `monthly_rent` DECIMAL(10,2) NOT NULL COMMENT '月租金',
  `payment_day` TINYINT NOT NULL DEFAULT 1 COMMENT '每月付款日'
);

2.2 前端技术方案

Vue3组合式API解决了传统Options API的代码碎片化问题。例如账单列表页:

javascript复制// 使用setup语法糖
const { pending, data: bills } = useAsyncData(
  'bills', 
  () => $fetch('/api/bills', {
    params: { 
      month: currentMonth.value,
      paymentStatus: filterStatus.value 
    }
  })
);

Element Plus的Table组件配合虚拟滚动,轻松应对万级数据渲染:

html复制<el-table 
  :data="bills"
  height="600"
  row-key="id"
  v-loading="pending">
  <el-table-column prop="roomNumber" label="房号" />
  <el-table-column prop="tenantName" label="租客" />
  <el-table-column prop="amount" label="金额" />
</el-table>

3. 核心功能实现细节

3.1 智能账单生成

采用策略模式处理不同租金计算规则:

java复制public interface RentCalculator {
    BigDecimal calculate(LeaseContract contract, LocalDate billDate);
}

// 普通月付实现
@Component
public class MonthlyCalculator implements RentCalculator {
    @Override
    public BigDecimal calculate(LeaseContract contract, LocalDate billDate) {
        return contract.getMonthlyRent();
    }
}

// 账单生成服务
@Service
@RequiredArgsConstructor
public class BillGenerateService {
    private final Map<String, RentCalculator> calculators;
    
    public void generateMonthlyBills() {
        List<LeaseContract> contracts = leaseContractMapper.selectActiveContracts();
        contracts.forEach(contract -> {
            BigDecimal amount = calculators.get(contract.getPaymentType())
                             .calculate(contract, LocalDate.now());
            // 生成账单记录...
        });
    }
}

3.2 支付状态同步

支付宝回调处理需注意:

  1. 验证签名防止伪造请求
  2. 处理幂等性问题(相同支付号只处理一次)
  3. 异步更新账单状态
java复制@PostMapping("/alipay/callback")
public String alipayCallback(@RequestBody String body) {
    // 1. 验证签名
    if (!AlipaySignature.rsaCheckV1(params, alipayPublicKey, "UTF-8")) {
        throw new IllegalStateException("签名验证失败");
    }
    
    // 2. 检查是否已处理
    String tradeNo = params.get("out_trade_no");
    if (paymentLogService.existsByTradeNo(tradeNo)) {
        return "success";
    }
    
    // 3. 异步更新
    eventPublisher.publishEvent(new PaymentSuccessEvent(tradeNo));
    return "success";
}

4. 性能优化实践

4.1 账单导出优化

当导出年度数据时(约10万条记录):

  1. 使用MyBatis流式查询避免OOM
  2. 通过Apache POI的SXSSFWorkbook实现分页写入
  3. 前端采用WebSocket推送进度
java复制public void exportBills(Long landlordId, HttpServletResponse response) {
    try (ScrollResult<Bill> scroll = billMapper.scrollByLandlord(landlordId)) {
        SXSSFWorkbook workbook = new SXSSFWorkbook(100); // 保持100行在内存中
        Sheet sheet = workbook.createSheet("账单");
        
        while (scroll.hasNext()) {
            Bill bill = scroll.next();
            // 写入行数据...
        }
        
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        workbook.write(response.getOutputStream());
    }
}

4.2 前端渲染加速

针对大型房源列表:

  1. 使用vue-virtual-scroller实现虚拟滚动
  2. 通过v-memo缓存静态节点
  3. 账单日历采用Canvas渲染
html复制<RecycleScroller
  class="scroller"
  :items="rooms"
  :item-size="72"
  key-field="id">
  <template #default="{ item }">
    <div v-memo="[item.id]">
      <RoomCard :room="item" />
    </div>
  </template>
</RecycleScroller>

5. 踩坑实录与解决方案

5.1 租金浮动计算陷阱

初期直接按30天折算日租金导致误差:

java复制// 错误做法(2月会多算)
BigDecimal dailyRent = monthlyRent.divide(BigDecimal.valueOf(30), 2);

// 正确做法(按实际月份天数)
LocalDate date = LocalDate.of(2023, 2, 1);
int days = date.lengthOfMonth();
BigDecimal dailyRent = monthlyRent.divide(BigDecimal.valueOf(days), 6, RoundingMode.HALF_UP);

5.2 时区导致的账单问题

服务器UTC时间导致本地日期错误:

  1. 统一使用Asia/Shanghai时区
  2. 前端传参带时区信息
  3. MySQL连接设置serverTimezone=Asia/Shanghai
yaml复制# application.yml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/rent_db?serverTimezone=Asia/Shanghai

5.3 微信支付证书加载

生产环境证书加载失败:

java复制// 正确加载方式(Spring资源路径)
@Value("classpath:/cert/apiclient_cert.p12")
private Resource certResource;

public WXPay initWxPay() throws Exception {
    WXPayConfig config = new WXPayConfig() {
        public InputStream getCertStream() {
            return certResource.getInputStream();
        }
        // 其他配置...
    };
    return new WXPay(config);
}

6. 安全防护措施

6.1 防越权查询

在Controller层添加租户过滤:

java复制@GetMapping("/bills")
public PageResult<BillVO> getBills(BillQuery query, @CurrentUser User user) {
    // 自动注入当前用户ID
    query.setLandlordId(user.getLandlordId()); 
    return billService.queryBills(query);
}

6.2 敏感数据脱敏

使用Jackson注解实现:

java复制public class TenantDTO {
    private Long id;
    private String name;
    
    @JsonSerialize(using = MobileSerializer.class)
    private String mobile;
}

// 自定义手机号序列化
public class MobileSerializer extends StdSerializer<String> {
    protected MobileSerializer() {
        super(String.class);
    }
    
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider provider) {
        gen.writeString(value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2"));
    }
}

这套系统经过三个月的迭代开发,目前稳定管理着超过2000套出租房源。最大的体会是:在租金计算这种涉及金钱的业务上,必须用BigDecimal处理金额(千万别用double),并且所有计算都要有单元测试覆盖边界情况。另外vue3的