1. 项目概述
这个基于SpringBoot+Android的智能化公寓运营平台,是我在去年完成的一个实际商业项目。当时客户需要一套能够同时满足物业管理和租户使用的综合性系统,经过三个月的开发迭代,最终交付的这套系统目前已经稳定运行了9个月,管理着12栋公寓楼共计800多套房间。
系统采用典型的B/S架构设计,后端使用SpringBoot框架,前端Android应用通过RESTful API与后端交互。数据库选用MySQL 8.0,主要考虑到其事务处理能力和对复杂查询的优化。整个系统包含六大核心模块:租户管理、房间管理、合同管理、缴费管理、报修管理和系统管理。
2. 技术架构设计
2.1 后端技术栈选型
选择SpringBoot作为后端框架主要基于以下几个考量:
- 快速开发:自动配置和起步依赖大大减少了样板代码
- 微服务友好:方便后续扩展为微服务架构
- 生态丰富:Spring生态有大量现成解决方案
核心依赖包括:
- Spring Web MVC:处理HTTP请求
- Spring Data JPA:数据库操作
- Spring Security:认证授权
- Lombok:减少样板代码
- MapStruct:对象映射
数据库设计遵循第三范式,主要表包括:
CREATE TABLE `apartment` ( `id` bigint NOT NULL AUTO_INCREMENT, `building_no` varchar(20) NOT NULL, `room_no` varchar(20) NOT NULL, `area` decimal(10,2) NOT NULL, `monthly_rent` decimal(10,2) NOT NULL, `status` tinyint NOT NULL COMMENT '0-空闲 1-已租 2-维修中', PRIMARY KEY (`id`), UNIQUE KEY `idx_building_room` (`building_no`,`room_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;2.2 移动端技术实现
Android端采用MVVM架构,主要组件包括:
- Retrofit:网络请求
- Room:本地缓存
- ViewModel:UI数据管理
- LiveData:数据观察
- Navigation:页面导航
一个典型的网络请求封装:
interface ApiService { @GET("repairs") suspend fun getRepairList(@Query("tenantId") tenantId: Long): Response<List<Repair>> } class RepairRepository { private val api = RetrofitClient.getInstance().create(ApiService::class.java) suspend fun fetchRepairs(tenantId: Long): Result<List<Repair>> { return try { val response = api.getRepairList(tenantId) if (response.isSuccessful) { Result.success(response.body()!!) } else { Result.failure(Exception("Network error")) } } catch (e: Exception) { Result.failure(e) } } }3. 核心功能实现
3.1 租赁管理模块
租赁流程包含以下步骤:
- 房源浏览与筛选
- 在线预约看房
- 电子合同签订
- 押金支付
- 钥匙领取
合同签署采用第三方电子签名服务集成:
public class ContractService { @Value("${esign.appId}") private String appId; @Value("${esign.appSecret}") private String appSecret; public String createSignUrl(Long contractId, Long tenantId) { // 调用电子签名平台API EsignClient client = new EsignClient(appId, appSecret); return client.createSignUrl(contractId, tenantId); } }3.2 报修管理流程
报修流程设计要点:
- 多媒介报修:支持文字、语音、图片
- 智能分派:根据维修工技能和位置自动分配
- 进度推送:实时状态更新
- 评价反馈:维修完成后满意度评价
报修状态机实现:
public enum RepairStatus { PENDING(0, "待接单"), ACCEPTED(1, "已接单"), PROCESSING(2, "处理中"), COMPLETED(3, "已完成"), CANCELLED(4, "已取消"); // 状态转换校验逻辑 public static boolean canTransfer(RepairStatus from, RepairStatus to) { // 实现状态转换规则 } }4. 关键技术难点与解决方案
4.1 并发支付处理
遇到的主要问题:高峰期租金支付时的并发冲突
解决方案:
- 乐观锁控制:
@Transactional public PaymentResult payRent(PaymentRequest request) { Lease lease = leaseRepository.findById(request.getLeaseId()) .orElseThrow(() -> new BusinessException("租赁合同不存在")); // 使用版本号控制并发 int updated = leaseRepository.updatePaymentStatus( lease.getId(), lease.getVersion(), PaymentStatus.PROCESSING); if (updated == 0) { throw new OptimisticLockException("支付状态更新冲突"); } // 调用支付网关 // ... }- 引入分布式锁:
public PaymentResult payRentDistributed(PaymentRequest request) { String lockKey = "payment:" + request.getLeaseId(); try { // 尝试获取锁,超时时间3秒 boolean locked = redisLock.tryLock(lockKey, 3, TimeUnit.SECONDS); if (!locked) { throw new BusinessException("系统繁忙,请稍后再试"); } // 处理支付逻辑 // ... } finally { redisLock.unlock(lockKey); } }4.2 移动端离线处理
Android端离线处理策略:
- 本地数据库缓存关键数据
- 操作队列存储待同步请求
- 网络恢复后自动同步
- 冲突解决策略(最后写入获胜)
Room数据库配置示例:
@Database(entities = [Repair::class, Payment::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun repairDao(): RepairDao companion object { @Volatile private var INSTANCE: AppDatabase? = null fun getInstance(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { INSTANCE ?: Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, "apartment.db" ).build().also { INSTANCE = it } } } } }5. 安全设计与实现
5.1 认证授权方案
采用JWT + Spring Security实现:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() .antMatchers("/api/tenant/**").hasRole("TENANT") .antMatchers("/api/staff/**").hasRole("STAFF") .antMatchers("/api/admin/**").hasRole("ADMIN") .anyRequest().authenticated() .and() .addFilter(new JwtAuthenticationFilter(authenticationManager())) .addFilter(new JwtAuthorizationFilter(authenticationManager())) .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS); } }5.2 敏感数据保护
- 数据库字段加密:
@Converter public class CryptoConverter implements AttributeConverter<String, String> { @Value("${encryption.key}") private String key; @Override public String convertToDatabaseColumn(String attribute) { // 使用AES加密 return AESUtil.encrypt(attribute, key); } @Override public String convertToEntityAttribute(String dbData) { return AESUtil.decrypt(dbData, key); } }- 接口敏感信息脱敏:
@JsonComponent public class SensitiveDataSerializer extends JsonSerializer<String> { @Override public void serialize(String value, JsonGenerator gen, SerializerProvider provider) throws IOException { if (value == null) { gen.writeNull(); return; } // 手机号脱敏:138****1234 if (value.matches("^1[3-9]\\d{9}$")) { gen.writeString(value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2")); } // 身份证脱敏 else if (value.matches("^[1-9]\\d{5}(18|19|20)\\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$")) { gen.writeString(value.replaceAll("(\\d{4})\\d{10}(\\w{4})", "$1**********$2")); } else { gen.writeString(value); } } }6. 性能优化实践
6.1 数据库优化
- 索引优化:
-- 为常用查询字段添加索引 ALTER TABLE payment ADD INDEX idx_tenant_date (tenant_id, payment_date); ALTER TABLE repair ADD INDEX idx_status_created (status, created_at);- 查询优化:
// 避免N+1查询 @Query("SELECT r FROM Repair r JOIN FETCH r.tenant WHERE r.status = :status") List<Repair> findByStatusWithTenant(@Param("status") RepairStatus status);6.2 接口性能提升
- 缓存策略:
@Cacheable(value = "apartmentCache", key = "#buildingNo+'-'+#roomNo") public Apartment getApartmentDetail(String buildingNo, String roomNo) { return apartmentRepository.findByBuildingNoAndRoomNo(buildingNo, roomNo) .orElseThrow(() -> new ResourceNotFoundException("房间不存在")); }- 批量操作优化:
@Transactional public void batchImportApartments(List<ApartmentImportDTO> dtos) { // 使用批量插入 jdbcTemplate.batchUpdate( "INSERT INTO apartment(building_no, room_no, area, monthly_rent, status) VALUES (?,?,?,?,0)", new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { ApartmentImportDTO dto = dtos.get(i); ps.setString(1, dto.getBuildingNo()); ps.setString(2, dto.getRoomNo()); ps.setBigDecimal(3, dto.getArea()); ps.setBigDecimal(4, dto.getMonthlyRent()); } @Override public int getBatchSize() { return dtos.size(); } }); }7. 项目部署方案
7.1 后端部署
采用Docker容器化部署:
FROM openjdk:11-jre-slim WORKDIR /app COPY target/apartment-management-0.0.1-SNAPSHOT.jar app.jar EXPOSE 8080 ENTRYPOINT ["java","-jar","app.jar"]使用docker-compose编排:
version: '3' services: app: build: . ports: - "8080:8080" environment: - SPRING_PROFILES_ACTIVE=prod - DB_URL=jdbc:mysql://mysql:3306/apartment depends_on: - mysql mysql: image: mysql:8.0 environment: - MYSQL_ROOT_PASSWORD=rootpass - MYSQL_DATABASE=apartment - MYSQL_USER=appuser - MYSQL_PASSWORD=apppass volumes: - mysql_data:/var/lib/mysql ports: - "3306:3306" volumes: mysql_data:7.2 移动端发布
Android发布流程:
- 生成签名密钥:
keytool -genkey -v -keystore apartment.jks -keyalg RSA -keysize 2048 -validity 10000 -alias apartment- 配置Gradle签名:
android { signingConfigs { release { storeFile file("apartment.jks") storePassword "yourpassword" keyAlias "apartment" keyPassword "yourpassword" } } buildTypes { release { signingConfig signingConfigs.release minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } }8. 项目演进方向
- 物联网集成:接入智能门锁、水电表等设备
- 数据分析:租金预测、维修预测等
- 微信小程序:扩展访问渠道
- 微服务化:按业务拆分服务
智能门锁集成示例:
public class SmartLockService { private final RestTemplate restTemplate; public void unlockDoor(Long apartmentId, String temporaryCode) { String url = "https://api.smartlock.com/v1/devices/{deviceId}/unlock"; Map<String, Object> request = new HashMap<>(); request.put("code", temporaryCode); request.put("duration", 30); // 30秒有效 restTemplate.postForObject( url, request, Void.class, getDeviceId(apartmentId)); } private String getDeviceId(Long apartmentId) { // 查询公寓对应的设备ID } }9. 开发经验总结
领域建模要准确:初期对租赁周期状态设计不够完善,导致后期频繁修改
移动端兼容性:测试要覆盖各种Android版本,特别是权限处理的变化
接口版本控制:从开始就要考虑API版本兼容问题
文档自动化:使用Swagger UI自动生成API文档
Swagger配置示例:
@Configuration @EnableSwagger2 public class SwaggerConfig { @Bean public Docket api() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage("com.example.apartment")) .paths(PathSelectors.any()) .build() .apiInfo(apiInfo()); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("公寓管理系统API") .description("包含移动端和后管的所有接口") .version("1.0") .build(); } }10. 常见问题排查
- Android端数据不同步问题:
- 检查网络状态监听是否正常
- 验证同步队列是否正常工作
- 查看冲突解决策略是否合理
- 支付状态不一致:
- 检查分布式锁的有效期
- 验证事务隔离级别
- 核对支付网关回调处理
- 性能瓶颈分析:
@Aspect @Component @Slf4j public class PerformanceMonitor { @Around("execution(* com.example.apartment..*.*(..))") public Object logPerformance(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long elapsed = System.currentTimeMillis() - start; if (elapsed > 500) { // 超过500ms记录警告 log.warn("Performance warning: {}.{} took {} ms", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName(), elapsed); } return result; } }