AI摘要

本文详细介绍了如何使用Spring Boot 3.3.5、MyBatis-Flex和SQLite构建零工平台后端系统。项目支持工作发布、申请匹配、用户管理、支付结算等业务场景,强调高性能、高可用和易扩展。文章涵盖了架构设计、技术选型、核心功能实现和生产部署,分享了JWT认证体系、统一结果封装、工作管理核心业务、文件上传服务等关键功能的实现细节,以及数据库设计、部署实战、监控与运维、性能优化等方面的经验总结。

本文将详细分享我使用Spring Boot 3.3.5 + MyBatis-Flex + SQLite构建零工平台后端系统的完整经验,包含架构设计、技术选型、核心功能实现和生产部署。

项目背景

随着零工经济的快速发展,我决定开发一个现代化的零工服务平台。后端系统需要支持工作发布、申请匹配、用户管理、支付结算等复杂业务场景,同时要保证高性能、高可用和易扩展。

项目概览

  • 项目名称: 智慧零工平台后端系统
  • 技术栈: Spring Boot 3.3.5 + Java 21 + MyBatis-Flex + SQLite
  • 项目地址: GitHub - Gigplatform_backend
  • API文档: 24个业务模块,100+接口
  • 部署方式: 宝塔面板 + PM2 + Nginx

技术选型分析

为什么选择Spring Boot 3?

  1. Java 21支持: 享受最新的语言特性和性能优化
  2. 原生镜像: 支持GraalVM原生编译,启动更快
  3. 响应式编程: 内置WebFlux支持
  4. 可观测性: 增强的监控和链路追踪
  5. 安全性: 升级的Spring Security

MyBatis-Flex vs MyBatis-Plus

经过对比,我选择了MyBatis-Flex:

// MyBatis-Flex 优势示例
// 1. 类型安全的查询
List<Job> jobs = queryWrapper
    .select(JOB.ALL_COLUMNS)
    .from(JOB)
    .where(JOB.STATUS.eq("ACTIVE"))
    .and(JOB.SALARY.ge(5000))
    .orderBy(JOB.CREATE_TIME.desc())
    .limit(10)
    .list();

// 2. 智能字段映射
@Table("t_job")
public class Job {
    @Id(keyType = KeyType.Auto)
    private Long id;

    @Column("job_title")
    private String title;

    // 自动处理驼峰命名
    private String companyName; // -> company_name
}

SQLite的选择

对于中小型项目,SQLite具有独特优势:

  • 零配置: 无需独立数据库服务器
  • 高性能: 本地文件访问,减少网络开销
  • 事务支持: 完整的ACID特性
  • 跨平台: 易于开发和部署
  • 成本低: 降低运维复杂度

架构设计

整体架构

┌─────────────────┐    ┌─────────────────┐    ┌─────────────────┐
│    前端应用      │    │   API网关       │    │   负载均衡       │
│  (uni-app)      │◄───│   (Nginx)       │◄───│                 │
└─────────────────┘    └─────────────────┘    └─────────────────┘
                                │
                                ▼
                    ┌─────────────────────────┐
                    │      Spring Boot        │
                    │    (业务逻辑层)          │
                    └─────────────────────────┘
                                │
                ┌───────────────┼───────────────┐
                ▼               ▼               ▼
        ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
        │  认证授权    │ │   业务服务  │  │   数据访问   │
        │  (JWT)      │ │  (Service)  │ │ (MyBatis)   │
        └─────────────┘ └─────────────┘ └─────────────┘
                                                │
                                                ▼
                                    ┌─────────────────┐
                                    │  SQLite 数据库   │
                                    └─────────────────┘

项目模块结构

com.example.backend/
├── controller/           # 控制器层
│   ├── AuthController           # 认证授权
│   ├── JobsController          # 工作管理  
│   ├── UsersController         # 用户管理
│   └── ...                     # 其他业务控制器
├── service/             # 业务逻辑层
│   ├── impl/           # 实现类
│   └── interfaces/     # 接口定义
├── mapper/             # 数据访问层
├── entity/             # 实体类
├── config/             # 配置类
├── common/             # 公共组件
│   ├── Result.java     # 统一返回结果
│   ├── JwtUtil.java    # JWT工具
│   └── JwtFilter.java  # JWT过滤器
└── BackendApplication.java # 启动类

核心功能实现

1. JWT认证体系

@Component
public class JwtUtil {

    @Value("${jwt.secret:default-secret}")
    private String secret;

    @Value("${jwt.expiration:86400}")
    private Long expiration;

    public String generateToken(String username) {
        Date expirationDate = new Date(System.currentTimeMillis() + expiration * 1000);

        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }

    public Claims getClaimsFromToken(String token) {
        try {
            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            return null;
        }
    }

    public boolean validateToken(String token) {
        try {
            Claims claims = getClaimsFromToken(token);
            return claims != null && !claims.getExpiration().before(new Date());
        } catch (Exception e) {
            return false;
        }
    }
}

// JWT过滤器
@Component
public class JwtFilter implements Filter {

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, 
                        FilterChain chain) throws IOException, ServletException {

        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        // 跳过认证的路径
        String path = httpRequest.getRequestURI();
        if (isPublicPath(path)) {
            chain.doFilter(request, response);
            return;
        }

        String token = extractToken(httpRequest);
        if (token == null || !jwtUtil.validateToken(token)) {
            httpResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
            httpResponse.getWriter().write(
                JSON.toJSONString(Result.error(102, "未登录,请先登录"))
            );
            return;
        }

        // 设置用户信息到上下文
        Claims claims = jwtUtil.getClaimsFromToken(token);
        LoginUser loginUser = new LoginUser();
        loginUser.setUsername(claims.getSubject());
        UserContext.setCurrentUser(loginUser);

        chain.doFilter(request, response);
    }
}

2. 统一结果封装

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result<T> {
    private Integer code;
    private String message;
    private T data;

    public static <T> Result<T> success() {
        return new Result<>(0, "调用成功", null);
    }

    public static <T> Result<T> success(T data) {
        return new Result<>(0, "调用成功", data);
    }

    public static <T> Result<T> error(Integer code, String message) {
        return new Result<>(code, message, null);
    }

    public static <T> Result<T> error(String message) {
        return new Result<>(200, message, null);
    }
}

// 全局异常处理
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public Result<String> handleException(Exception e) {
        log.error("系统异常", e);
        return Result.error("系统繁忙,请稍后重试");
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<String> handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldError().getDefaultMessage();
        return Result.error(220, message);
    }

    @ExceptionHandler(IllegalArgumentException.class)
    public Result<String> handleIllegalArgumentException(IllegalArgumentException e) {
        return Result.error(220, e.getMessage());
    }
}

3. 工作管理核心业务

@RestController
@RequestMapping("/api/jobs")
@Slf4j
public class JobsController {

    @Autowired
    private JobsService jobsService;

    // 获取工作列表(支持多条件筛选)
    @GetMapping("/latest/list")
    public Result<Map<String, Object>> getJobList(
            @RequestParam(defaultValue = "1") int page,
            @RequestParam(defaultValue = "10") int size,
            @RequestParam(required = false) String keyword,
            @RequestParam(required = false) Long categoryId,
            @RequestParam(required = false) String location,
            @RequestParam(required = false) String salaryRange) {

        try {
            Page<Job> jobPage = jobsService.getJobList(
                page, size, keyword, categoryId, location, salaryRange
            );

            Map<String, Object> result = new HashMap<>();
            result.put("list", jobPage.getRecords());
            result.put("total", jobPage.getTotal());
            result.put("pages", jobPage.getPages());
            result.put("current", jobPage.getCurrent());

            return Result.success(result);
        } catch (Exception e) {
            log.error("获取工作列表失败", e);
            return Result.error("获取工作列表失败");
        }
    }

    // 申请工作
    @PostMapping("/apply/{jobId}")
    public Result<String> applyForJob(@PathVariable Long jobId) {
        try {
            LoginUser currentUser = UserContext.getCurrentUser();

            // 获取工人资料
            WorkerProfile profile = workerProfilesService.getByUserId(currentUser.getUserId());
            if (profile == null) {
                return Result.error(220, "请先完善工人资料");
            }

            // 检查是否已申请
            boolean hasApplied = jobApplicationsService.hasApplied(jobId, profile.getId());
            if (hasApplied) {
                return Result.error(220, "您已申请过该工作");
            }

            // 创建申请记录
            JobApplication application = new JobApplication();
            application.setJobId(jobId);
            application.setWorkerId(profile.getId());
            application.setStatus("PENDING");
            application.setApplyTime(new Date());

            jobApplicationsService.save(application);

            // 更新工作申请数量
            jobsService.incrementApplyCount(jobId);

            return Result.success("申请成功");

        } catch (Exception e) {
            log.error("申请工作失败, jobId: {}", jobId, e);
            return Result.error("申请失败,请稍后重试");
        }
    }
}

// 业务服务实现
@Service
@Transactional
public class JobsServiceImpl implements JobsService {

    @Autowired
    private JobsMapper jobsMapper;

    @Override
    public Page<Job> getJobList(int page, int size, String keyword, 
                               Long categoryId, String location, String salaryRange) {

        QueryWrapper query = QueryWrapper.create()
                .select(JOB.ALL_COLUMNS)
                .from(JOB)
                .where(JOB.STATUS.eq("ACTIVE"));

        // 关键词搜索
        if (StringUtils.hasText(keyword)) {
            query.and(JOB.TITLE.like(keyword)
                    .or(JOB.DESCRIPTION.like(keyword))
                    .or(JOB.COMPANY_NAME.like(keyword)));
        }

        // 分类筛选
        if (categoryId != null) {
            query.and(JOB.CATEGORY_ID.eq(categoryId));
        }

        // 地区筛选
        if (StringUtils.hasText(location)) {
            query.and(JOB.LOCATION.like(location));
        }

        // 薪资范围筛选
        if (StringUtils.hasText(salaryRange)) {
            applySalaryFilter(query, salaryRange);
        }

        query.orderBy(JOB.CREATE_TIME.desc());

        return jobsMapper.paginate(page, size, query);
    }

    private void applySalaryFilter(QueryWrapper query, String salaryRange) {
        switch (salaryRange) {
            case "0-3000":
                query.and(JOB.SALARY_MIN.le(3000));
                break;
            case "3000-5000":
                query.and(JOB.SALARY_MIN.ge(3000).and(JOB.SALARY_MAX.le(5000)));
                break;
            case "5000-8000":
                query.and(JOB.SALARY_MIN.ge(5000).and(JOB.SALARY_MAX.le(8000)));
                break;
            case "8000+":
                query.and(JOB.SALARY_MIN.ge(8000));
                break;
        }
    }
}

4. 文件上传服务

@RestController
@RequestMapping("/api/upload")
@Slf4j
public class UploadController {

    @Value("${file.upload.dir:uploads}")
    private String uploadDir;

    @Value("${file.upload.max-size:10485760}") // 10MB
    private long maxFileSize;

    @PostMapping("/image")
    public Result<Map<String, String>> uploadImage(@RequestParam("file") MultipartFile file) {
        try {
            // 验证文件
            if (file.isEmpty()) {
                return Result.error(220, "文件不能为空");
            }

            if (file.getSize() > maxFileSize) {
                return Result.error(220, "文件大小不能超过10MB");
            }

            // 验证文件类型
            String contentType = file.getContentType();
            if (!isValidImageType(contentType)) {
                return Result.error(220, "只支持 jpg、png、gif 格式的图片");
            }

            // 生成文件名
            String originalFilename = file.getOriginalFilename();
            String extension = getFileExtension(originalFilename);
            String filename = generateFileName() + "." + extension;

            // 创建目录
            Path uploadPath = Paths.get(uploadDir);
            if (!Files.exists(uploadPath)) {
                Files.createDirectories(uploadPath);
            }

            // 保存文件
            Path filePath = uploadPath.resolve(filename);
            Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);

            // 返回访问URL
            String fileUrl = "/uploads/" + filename;
            Map<String, String> result = new HashMap<>();
            result.put("url", fileUrl);
            result.put("filename", filename);

            return Result.success(result);

        } catch (Exception e) {
            log.error("文件上传失败", e);
            return Result.error("文件上传失败");
        }
    }

    private boolean isValidImageType(String contentType) {
        return contentType != null && (
                contentType.equals("image/jpeg") ||
                contentType.equals("image/png") ||
                contentType.equals("image/gif")
        );
    }

    private String generateFileName() {
        return System.currentTimeMillis() + "_" + 
               ThreadLocalRandom.current().nextInt(1000, 9999);
    }
}

数据库设计

核心表结构

-- 用户表
CREATE TABLE users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    username VARCHAR(50) NOT NULL UNIQUE,
    password VARCHAR(255) NOT NULL,
    email VARCHAR(100),
    phone VARCHAR(20) NOT NULL,
    user_type VARCHAR(20) NOT NULL, -- 'employer' 或 'employee'
    avatar VARCHAR(255),
    status VARCHAR(20) DEFAULT 'ACTIVE',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

-- 工作表
CREATE TABLE jobs (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title VARCHAR(200) NOT NULL,
    description TEXT,
    category_id INTEGER,
    employer_id INTEGER NOT NULL,
    company_name VARCHAR(100),
    location VARCHAR(100),
    salary_min DECIMAL(10,2),
    salary_max DECIMAL(10,2),
    salary_type VARCHAR(20), -- 'hourly', 'daily', 'monthly'
    requirements TEXT,
    contact_info VARCHAR(255),
    status VARCHAR(20) DEFAULT 'ACTIVE',
    apply_count INTEGER DEFAULT 0,
    view_count INTEGER DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (employer_id) REFERENCES users(id),
    FOREIGN KEY (category_id) REFERENCES categories(id)
);

-- 工作申请表
CREATE TABLE job_applications (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    job_id INTEGER NOT NULL,
    worker_id INTEGER NOT NULL,
    status VARCHAR(20) DEFAULT 'PENDING', -- PENDING, APPROVED, REJECTED
    apply_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    response_time DATETIME,
    employer_comment TEXT,
    FOREIGN KEY (job_id) REFERENCES jobs(id),
    FOREIGN KEY (worker_id) REFERENCES worker_profiles(id),
    UNIQUE(job_id, worker_id)
);

-- 工人资料表
CREATE TABLE worker_profiles (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL UNIQUE,
    real_name VARCHAR(50),
    age INTEGER,
    gender VARCHAR(10),
    education VARCHAR(50),
    experience_years INTEGER,
    skills TEXT, -- JSON格式存储技能列表
    resume TEXT,
    certification VARCHAR(255),
    location VARCHAR(100),
    available_time VARCHAR(255),
    hourly_rate DECIMAL(8,2),
    rating DECIMAL(3,2) DEFAULT 0,
    completed_jobs INTEGER DEFAULT 0,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

索引优化

-- 性能优化索引
CREATE INDEX idx_jobs_status_created ON jobs(status, created_at);
CREATE INDEX idx_jobs_category_location ON jobs(category_id, location);
CREATE INDEX idx_jobs_employer ON jobs(employer_id);
CREATE INDEX idx_applications_job_worker ON job_applications(job_id, worker_id);
CREATE INDEX idx_applications_status ON job_applications(status);
CREATE INDEX idx_worker_profiles_user ON worker_profiles(user_id);
CREATE INDEX idx_worker_profiles_location ON worker_profiles(location);

部署实战

1. 宝塔面板部署

创建部署脚本:

#!/bin/bash
# deploy.sh - 自动化部署脚本

echo "=== 智慧零工平台后端部署脚本 ==="

# 1. 环境检查
check_environment() {
    echo "检查部署环境..."

    # 检查Java版本
    if ! java -version 2>&1 | grep "21\." > /dev/null; then
        echo "错误: 需要Java 21"
        exit 1
    fi

    # 检查PM2
    if ! command -v pm2 &> /dev/null; then
        echo "安装PM2..."
        npm install -g pm2
    fi

    echo "环境检查完成"
}

# 2. 备份数据
backup_data() {
    echo "备份数据库..."
    if [ -f "/www/wwwroot/smartgigplatform.db" ]; then
        cp /www/wwwroot/smartgigplatform.db \
           /www/wwwroot/backup/smartgigplatform_$(date +%Y%m%d_%H%M%S).db
        echo "数据库备份完成"
    fi
}

# 3. 部署应用
deploy_app() {
    echo "部署应用..."

    # 停止旧版本
    pm2 stop smart-gig-platform 2>/dev/null || true

    # 部署新版本
    cp smart-gig-platformbackend-0.0.1-SNAPSHOT.jar /www/wwwroot/
    cp application-prod.properties /www/wwwroot/
    cp ecosystem.config.js /www/wwwroot/

    # 设置权限
    chown www:www /www/wwwroot/smart-gig-platformbackend-0.0.1-SNAPSHOT.jar
    chmod 755 /www/wwwroot/smart-gig-platformbackend-0.0.1-SNAPSHOT.jar

    # 启动应用
    cd /www/wwwroot
    pm2 start ecosystem.config.js

    echo "应用部署完成"
}

# 4. 健康检查
health_check() {
    echo "进行健康检查..."

    sleep 10

    # 检查进程状态
    if pm2 show smart-gig-platform | grep "online" > /dev/null; then
        echo "✅ 应用启动成功"
    else
        echo "❌ 应用启动失败"
        pm2 logs smart-gig-platform --lines 20
        exit 1
    fi

    # 检查API接口
    if curl -s http://localhost:8081/actuator/health | grep "UP" > /dev/null; then
        echo "✅ API健康检查通过"
    else
        echo "❌ API健康检查失败"
        exit 1
    fi
}

# 执行部署
main() {
    check_environment
    backup_data
    deploy_app
    health_check

    echo "=== 部署完成 ==="
    echo "应用状态: http://localhost:8081/actuator/health"
    echo "日志查看: pm2 logs smart-gig-platform"
}

main "$@"

2. PM2进程管理

// ecosystem.config.js
module.exports = {
  apps: [{
    name: 'smart-gig-platform',
    script: '/www/server/java/jdk-21.0.2/bin/java',
    args: [
      '-server',
      '-Xms512m',
      '-Xmx1024m',
      '-Dspring.profiles.active=prod',
      '-Dfile.encoding=UTF-8',
      '-jar',
      'smart-gig-platformbackend-0.0.1-SNAPSHOT.jar'
    ],
    cwd: '/www/wwwroot',
    instances: 1,
    autorestart: true,
    watch: false,
    max_memory_restart: '1G',
    env: {
      NODE_ENV: 'production'
    },
    log_file: '/www/wwwroot/logs/app.log',
    out_file: '/www/wwwroot/logs/app-out.log',
    error_file: '/www/wwwroot/logs/app-error.log',
    log_date_format: 'YYYY-MM-DD HH:mm:ss',
    merge_logs: true
  }]
}

3. Nginx配置

# lgpt.ybyq.wang.conf
server {
    listen 80;
    server_name lgpt.ybyq.wang;
    root /www/wwwroot/lgpt.ybyq.wang/h5;
    index index.html;

    # API代理
    location /api/ {
        proxy_pass http://127.0.0.1:8081/api/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # 超时设置
        proxy_connect_timeout 30s;
        proxy_send_timeout 30s;
        proxy_read_timeout 30s;

        # 缓冲设置
        proxy_buffering on;
        proxy_buffer_size 8k;
        proxy_buffers 8 8k;
    }

    # 文件上传代理
    location /uploads/ {
        alias /www/wwwroot/uploads/;
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # SPA路由支持
    location / {
        try_files $uri $uri/ /index.html;
    }

    # 静态资源缓存
    location ~* \.(js|css|png|jpg|jpeg|gif|svg|woff2)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }

    # 安全头
    add_header X-Frame-Options "SAMEORIGIN";
    add_header X-Content-Type-Options "nosniff";
    add_header X-XSS-Protection "1; mode=block";

    # Gzip压缩
    gzip on;
    gzip_vary on;
    gzip_min_length 1k;
    gzip_comp_level 6;
    gzip_types
        application/json
        application/javascript
        text/css
        text/javascript
        text/plain;

    # 访问日志
    access_log /www/wwwlogs/lgpt.ybyq.wang.log;
    error_log /www/wwwlogs/lgpt.ybyq.wang.error.log;
}

监控与运维

1. 应用监控

// 自定义健康检查
@Component
public class CustomHealthIndicator implements HealthIndicator {

    @Autowired
    private DataSource dataSource;

    @Override
    public Health health() {
        try {
            // 检查数据库连接
            try (Connection connection = dataSource.getConnection()) {
                boolean isValid = connection.isValid(5);
                if (isValid) {
                    return Health.up()
                            .withDetail("database", "可用")
                            .withDetail("connection_pool", getConnectionPoolInfo())
                            .build();
                }
            }
        } catch (Exception e) {
            return Health.down()
                    .withDetail("database", "不可用")
                    .withDetail("error", e.getMessage())
                    .build();
        }

        return Health.down().build();
    }

    private Map<String, Object> getConnectionPoolInfo() {
        Map<String, Object> info = new HashMap<>();
        info.put("active", "查询活跃连接数");
        info.put("idle", "查询空闲连接数");
        return info;
    }
}

// 自定义指标
@Component
public class BusinessMetrics {

    private final MeterRegistry meterRegistry;
    private final Counter jobCreatedCounter;
    private final Counter jobAppliedCounter;

    public BusinessMetrics(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
        this.jobCreatedCounter = Counter.builder("jobs.created")
                .description("创建的工作数量")
                .register(meterRegistry);
        this.jobAppliedCounter = Counter.builder("jobs.applied")
                .description("申请的工作数量")
                .register(meterRegistry);
    }

    public void incrementJobCreated() {
        jobCreatedCounter.increment();
    }

    public void incrementJobApplied() {
        jobAppliedCounter.increment();
    }
}

2. 日志管理

# application-prod.properties
logging:
  level:
    com.example.backend: INFO
    org.springframework.web: WARN
    org.springframework.security: WARN
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n"
    file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n"
  file:
    name: /www/wwwroot/logs/application.log
    max-size: 100MB
    max-history: 30

3. 备份策略

#!/bin/bash
# backup.sh - 数据备份脚本

BACKUP_DIR="/www/wwwroot/backup"
DB_FILE="/www/wwwroot/smartgigplatform.db"
UPLOAD_DIR="/www/wwwroot/uploads"

# 创建备份目录
mkdir -p $BACKUP_DIR

# 数据库备份
backup_database() {
    local timestamp=$(date +%Y%m%d_%H%M%S)
    local backup_file="$BACKUP_DIR/smartgigplatform_$timestamp.db"

    echo "备份数据库..."
    cp $DB_FILE $backup_file
    gzip $backup_file

    echo "数据库备份完成: $backup_file.gz"
}

# 文件备份
backup_uploads() {
    local timestamp=$(date +%Y%m%d_%H%M%S)
    local backup_file="$BACKUP_DIR/uploads_$timestamp.tar.gz"

    echo "备份上传文件..."
    tar -czf $backup_file -C /www/wwwroot uploads/

    echo "文件备份完成: $backup_file"
}

# 清理旧备份(保留7天)
cleanup_old_backups() {
    echo "清理旧备份文件..."
    find $BACKUP_DIR -name "*.gz" -mtime +7 -delete
    echo "清理完成"
}

# 执行备份
main() {
    echo "=== 开始备份 $(date) ==="
    backup_database
    backup_uploads
    cleanup_old_backups
    echo "=== 备份完成 $(date) ==="
}

main

性能优化

1. 数据库查询优化

// 使用MyBatis-Flex的批量操作
@Service
public class JobsBatchService {

    @Autowired
    private JobsMapper jobsMapper;

    // 批量插入工作
    @Transactional
    public void batchCreateJobs(List<Job> jobs) {
        // 使用MyBatis-Flex的批量插入
        jobsMapper.insertBatch(jobs);
    }

    // 批量更新状态
    @Transactional
    public void batchUpdateStatus(List<Long> jobIds, String status) {
        UpdateWrapper updateWrapper = UpdateWrapper.create()
                .set(JOB.STATUS, status)
                .set(JOB.UPDATED_AT, new Date())
                .where(JOB.ID.in(jobIds));

        jobsMapper.update(null, updateWrapper);
    }

    // 分页查询优化
    public Page<JobVO> getJobsWithDetails(int page, int size) {
        // 使用关联查询减少N+1问题
        QueryWrapper query = QueryWrapper.create()
                .select(
                    JOB.ALL_COLUMNS,
                    CATEGORY.NAME.as("categoryName"),
                    USER.USERNAME.as("employerName")
                )
                .from(JOB)
                .leftJoin(CATEGORY).on(JOB.CATEGORY_ID.eq(CATEGORY.ID))
                .leftJoin(USER).on(JOB.EMPLOYER_ID.eq(USER.ID))
                .where(JOB.STATUS.eq("ACTIVE"))
                .orderBy(JOB.CREATE_TIME.desc());

        return jobsMapper.paginate(page, size, query);
    }
}

2. 缓存策略

// Redis缓存配置
@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        RedisCacheManager.Builder builder = RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(redisConnectionFactory())
                .cacheDefaults(cacheConfiguration());

        return builder.build();
    }

    private RedisCacheConfiguration cacheConfiguration() {
        return RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofMinutes(30))
                .serializeKeysWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new StringRedisSerializer()))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                        .fromSerializer(new GenericJackson2JsonRedisSerializer()));
    }
}

// 使用缓存
@Service
public class JobsCacheService {

    @Cacheable(value = "jobs", key = "#jobId")
    public Job getJobById(Long jobId) {
        return jobsMapper.selectOneById(jobId);
    }

    @Cacheable(value = "job-list", key = "#page + '_' + #size + '_' + #keyword")
    public Page<Job> getJobList(int page, int size, String keyword) {
        // 查询逻辑
        return jobsMapper.paginate(page, size, query);
    }

    @CacheEvict(value = "jobs", key = "#job.id")
    public void updateJob(Job job) {
        jobsMapper.update(job);
    }

    @CacheEvict(value = {"jobs", "job-list"}, allEntries = true)
    public void clearAllCache() {
        // 清空所有缓存
    }
}

开发经验总结

最佳实践

  1. 分层架构: 严格按照Controller-Service-Mapper分层
  2. 异常处理: 统一异常处理和错误码管理
  3. 参数校验: 使用@Valid注解进行参数验证
  4. 事务管理: 合理使用@Transactional注解
  5. 日志记录: 关键操作添加日志记录
  6. API文档: 使用Swagger生成API文档

踩过的坑

  1. SQLite并发问题: 使用连接池解决并发访问
  2. JWT安全性: 定期更换密钥,设置合理过期时间
  3. 文件上传: 限制文件大小和类型,防止恶意上传
  4. 数据库备份: 定期备份,避免数据丢失
  5. 性能优化: 合理使用索引,避免N+1查询问题

结语

通过这次Spring Boot 3 + MyBatis-Flex的实践,我深入体验了现代Java开发的便利性。项目从零到生产部署,积累了丰富的实战经验。

关键收获包括:

  • 现代化技术栈能显著提升开发效率
  • 合理的架构设计是项目成功的基石
  • 性能优化需要在开发过程中持续关注
  • 监控运维对生产环境稳定性至关重要

希望这篇文章能为Java后端开发者提供一些参考和启发。


项目链接:

欢迎交流讨论!邮箱: 676567473@qq.com

如果觉得我的文章对你有用,请随意赞赏
END
本文作者:
文章标题:智慧零工平台后端开发实战:Spring Boot 3 + MyBatis-Flex 现代化架构
本文地址:https://blog.ybyq.wang/archives/543.html
版权说明:若无注明,本文皆Xuan's blog原创,转载请保留文章出处。