# 阶段2a架构设计 — 果农核心功能 > 基于 Team A 需求规格(阶段2a spec.md) > 新增模块:果农名片、招工发布、找工人、找客商、后台审核 --- ## 一、系统架构(继承阶段一 + 扩展) ### 1.1 整体架构 ``` ┌─────────────────────────────────────────────────────────────┐ │ 客户端层 │ ├─────────────────────────┬───────────────────────────────────┤ │ 微信小程序 (wxapp) │ 后台管理系统 (wxbackstage) │ │ - 原生微信开发 │ - Vue 2 + Element UI │ │ - 面向四类用户 │ - 面向政府管理部门 │ └───────────┬─────────────┴───────────────┬───────────────────┘ │ │ ▼ ▼ ┌─────────────────────────────────────────────────────────────┐ │ API 网关层 │ │ - JWT Token 认证 │ │ - 权限拦截器(RBAC) │ │ - 接口路径:/api/wx/* (小程序) /api/admin/* (后台) │ └───────────────────────────┬─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 业务逻辑层 │ ├──────────┬──────────┬──────────┬──────────┬────────────────┤ │ 用户模块 │ 系统模块 │ 审核模块 │ 首页模块 │ 通用模块 │ │AuthService│SysService│AuditService│HomeService│CommonService│ ├──────────┼──────────┼──────────┼──────────┼────────────────┤ │ 果农模块 │ 招工模块 │ 搜索模块 │ 文件模块 │ 工具模块 │ │GrowerSvc │RecruitSvc│SearchSvc │FileService│ DistanceUtil │ │GrowerAudit│RecruitRev│CallLogSvc│ │ KeywordCheck │ └──────┬───┴──────┬───┴──────┬───┴──────┬───┴────────┬───────┘ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 数据访问层 │ │ - MyBatis XML 映射 │ │ - Mapper 接口 │ └───────────────────────────┬─────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────┐ │ 数据存储层 │ ├─────────────────────────┬───────────────────────────────────┤ │ MySQL (sayu 库) │ 本地文件存储 │ │ - 20 张业务表 │ - ./uploads/image/ │ │ - AES 加密字段 │ - ./uploads/video/ │ │ - SHA256 哈希索引 │ │ └─────────────────────────┴───────────────────────────────────┘ ``` ### 1.2 阶段2a新增模块 | 模块 | 包名 | 职责 | |------|------|------| | 果农模块 | com.sayu.service | 果农档案CRUD、审核状态管理 | | 招工模块 | com.sayu.service | 招工发布、编辑、下架、敏感词检测 | | 搜索模块 | com.sayu.service | 工人列表、客商列表、距离计算 | | 文件模块 | com.sayu.service | 文件上传、存储、访问、删除 | | 审核模块(扩展) | com.sayu.service | 名片审核、待复核列表、SLA提醒 | ### 1.3 分层架构(继承阶段一) ``` Controller 层(API 入口) ↓ Service 层(业务逻辑) ↓ Mapper 层(数据访问) ↓ Entity/DTO/VO 层(数据模型) ``` --- ## 二、数据库设计(继承阶段一 + 扩展) ### 2.1 ER 图(阶段2a涉及) ``` sys_user (用户基础表) │ └── 1:N ──→ user_identity (身份关联表) │ ├── 1:1 ──→ grower_profile (果农档案) ←── 阶段2a核心 │ │ │ └── 1:N ──→ recruit_info (招工信息) ←── 阶段2a核心 │ ├── 1:1 ──→ worker_profile (工人档案) │ │ │ └── 1:N ──→ worker_apply (工人报名) │ └── 1:1 ──→ buyer_profile (客商档案) call_log (拨号日志) ←── 阶段2a新增使用 │ ├── caller_identity_id → user_identity └── callee_identity_id → user_identity audit_log (审核日志) ←── 阶段2a扩展使用 │ └── target_type = 'GROWER_PROFILE' / 'RECRUIT_REVIEW' ``` ### 2.2 阶段2a涉及的核心表 #### grower_profile(果农档案表)— 阶段2a核心 | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| | id | BIGINT | PK, AUTO_INCREMENT | 主键 | | user_identity_id | BIGINT | INDEX | 关联user_identity.id | | name | VARCHAR(50) | | 姓名 | | varieties | VARCHAR(200) | | 苹果品种(JSON数组) | | yield_amount | DECIMAL(10,2) | | 产量(斤) | | expected_price | DECIMAL(8,2) | | 预期价格(元/斤) | | address | VARCHAR(200) | | 果园地址 | | latitude | DECIMAL(10,7) | INDEX | 纬度 | | longitude | DECIMAL(10,7) | INDEX | 经度 | | video_url | VARCHAR(255) | | 果园视频URL(本地存储) | | photos | TEXT | | 照片URL(JSON数组) | | audit_status | TINYINT | INDEX | 0待审 1通过 2驳回 | | audit_remark | VARCHAR(200) | | 驳回原因 | #### recruit_info(招工信息表)— 阶段2a核心 | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| | id | BIGINT | PK, AUTO_INCREMENT | 主键 | | user_identity_id | BIGINT | INDEX | 发布者身份ID | | work_types | VARCHAR(100) | | 工种(JSON数组) | | price | DECIMAL(8,2) | | 价格 | | price_unit | VARCHAR(10) | | DAY/PIECE | | worker_count | INT | | 需要人数 | | days | INT | | 天数 | | location | VARCHAR(200) | | 工作地点 | | latitude | DECIMAL(10,7) | INDEX | 纬度 | | longitude | DECIMAL(10,7) | INDEX | 经度 | | remark | VARCHAR(500) | | 备注 | | status | TINYINT | INDEX | 0下架 1发布中 | | keyword_flag | TINYINT | INDEX | 0正常 1待复核 | | created_at | DATETIME | | 发布时间 | #### call_log(拨号日志表)— 阶段2a新增使用 | 字段 | 类型 | 约束 | 说明 | |------|------|------|------| | id | BIGINT | PK, AUTO_INCREMENT | 主键 | | caller_identity_id | BIGINT | INDEX | 拨号方身份ID | | callee_identity_id | BIGINT | INDEX | 被拨方身份ID | | call_time | DATETIME | | 拨号时间 | ### 2.3 索引设计(阶段2a新增) ```sql -- grower_profile 表索引 CREATE INDEX idx_user_identity_id ON grower_profile(user_identity_id); CREATE INDEX idx_audit_status ON grower_profile(audit_status); CREATE INDEX idx_lat_lng ON grower_profile(latitude, longitude); -- recruit_info 表索引 CREATE INDEX idx_user_identity_id ON recruit_info(user_identity_id); CREATE INDEX idx_status ON recruit_info(status); CREATE INDEX idx_keyword_flag ON recruit_info(keyword_flag); CREATE INDEX idx_lat_lng ON recruit_info(latitude, longitude); -- call_log 表索引 CREATE INDEX idx_caller ON call_log(caller_identity_id); CREATE INDEX idx_callee ON call_log(callee_identity_id); ``` --- ## 三、接口设计(阶段2a新增) ### 3.1 果农名片接口 #### GET /api/wx/grower/profile — 获取果农档案 **描述**: 获取当前果农的名片信息 **请求头**: `Authorization: Bearer {token}` **响应参数**: ```json { "code": 200, "message": "success", "data": { "id": 1, "name": "张三", "varieties": ["红富士", "嘎啦"], "yieldAmount": 50000, "expectedPrice": 4.0, "address": "洒渔镇黄兴村", "latitude": 27.3331, "longitude": 103.7168, "videoUrl": "http://localhost:8080/files/video/abc.mp4", "photos": ["http://localhost:8080/files/image/1.jpg"], "auditStatus": 0, "auditRemark": null } } ``` #### PUT /api/wx/grower/profile — 更新果农档案 **描述**: 更新果农名片信息,审核状态重置为待审 **请求参数**: ```json { "name": "张三", "varieties": ["红富士", "嘎啦"], "yieldAmount": 50000, "expectedPrice": 4.0, "address": "洒渔镇黄兴村", "latitude": 27.3331, "longitude": 103.7168, "videoUrl": "http://localhost:8080/files/video/abc.mp4", "photos": ["http://localhost:8080/files/image/1.jpg"] } ``` **业务逻辑**: 1. 验证必填字段 2. 更新 grower_profile 3. 重置 audit_status=0,清空 audit_remark --- ### 3.2 招工接口 #### POST /api/wx/grower/recruit — 发布招工 **描述**: 发布招工信息,免审核直接发布 **请求参数**: ```json { "workTypes": ["采摘工", "分拣工"], "price": 150, "priceUnit": "DAY", "workerCount": 3, "days": 5, "location": "洒渔镇黄兴村", "latitude": 27.3331, "longitude": 103.7168, "remark": "需要有经验的采摘工" } ``` **业务逻辑**: 1. 验证必填字段(workTypes、price、priceUnit、workerCount、location) 2. 敏感词检测(keyword_flag) 3. 创建 recruit_info(status=1,免审核) #### GET /api/wx/grower/recruit — 我的招工列表 **请求参数**: `?page=1&pageSize=20` **响应参数**: ```json { "code": 200, "message": "success", "data": { "total": 5, "page": 1, "pageSize": 20, "list": [ { "id": 1, "workTypes": ["采摘工"], "price": 150, "priceUnit": "DAY", "workerCount": 3, "days": 5, "location": "洒渔镇黄兴村", "status": 1, "keywordFlag": 0, "applyCount": 2, "createdAt": "2026-05-30 10:00" } ] } } ``` #### PUT /api/wx/grower/recruit/{id} — 编辑招工 **请求参数**: ```json { "price": 180, "workerCount": 5, "remark": "价格上调,急招" } ``` **业务逻辑**: 1. 验证招工存在且属于当前用户 2. 更新可编辑字段 3. 重新触发敏感词检测 #### DELETE /api/wx/grower/recruit/{id} — 下架招工 **业务逻辑**: 1. 验证招工存在且属于当前用户 2. 更新 status=0(逻辑删除) --- ### 3.3 找工人/客商接口 #### GET /api/wx/grower/workers — 工人列表 **请求参数**: `?workType=采摘工&page=1&pageSize=20` **响应参数**: ```json { "code": 200, "message": "success", "data": { "total": 10, "page": 1, "pageSize": 20, "list": [ { "identityId": 5, "name": "李四", "skills": ["采摘工", "分拣工"], "price": 150, "priceUnit": "DAY", "status": 1, "distance": 2.5 } ] } } ``` **业务逻辑**: 1. 查询 status=1(空闲)的工人 2. 可选按工种筛选(skills JSON字段 LIKE 查询) 3. 计算距离(Haversine公式) 4. 按距离升序排序 #### GET /api/wx/grower/buyers — 客商列表 **请求参数**: `?page=1&pageSize=20` **响应参数**: ```json { "code": 200, "message": "success", "data": { "total": 5, "page": 1, "pageSize": 20, "list": [ { "identityId": 10, "name": "王五果业", "varieties": ["红富士", "嘎啦"], "priceRange": "3.5-4.5", "totalAmount": 100000, "standards": "果径≥80mm", "address": "昭通市水果批发市场" } ] } } ``` --- ### 3.4 拨号接口 #### POST /api/wx/call/phone — 拨号接口 **描述**: 获取对方手机号并记录拨号日志 **请求参数**: ```json { "calleeIdentityId": 5 } ``` **响应参数**: ```json { "code": 200, "message": "success", "data": { "phone": "138****8888", "realPhone": "13800138000" } } ``` **业务逻辑**: 1. 验证被拨方存在 2. 记录拨号日志(call_log) 3. 返回脱敏手机号给前端显示,真实手机号用于拨号 --- ### 3.5 后台审核接口 #### GET /api/admin/audit/grower-profile — 名片审核列表 **请求参数**: `?page=1&pageSize=10&status=0` **响应参数**: ```json { "code": 200, "message": "success", "data": { "total": 3, "page": 1, "pageSize": 10, "list": [ { "id": 1, "growerName": "张三", "varieties": ["红富士"], "yieldAmount": 50000, "videoUrl": "...", "photos": ["..."], "auditStatus": 0, "createdAt": "2026-05-30 10:00" } ] } } ``` #### PUT /api/admin/audit/grower-profile/{id} — 名片审核操作 **请求参数**: ```json { "action": "APPROVE", "reason": "" } ``` 或驳回: ```json { "action": "REJECT", "reason": "视频内容不清晰,请重新上传" } ``` **业务逻辑**: 1. 验证审核记录存在 2. 驳回时 reason 不能为空 3. 更新 grower_profile.audit_status 4. 记录审核日志(audit_log) #### GET /api/admin/audit/recruit-review — 待复核招工列表 **请求参数**: `?page=1&pageSize=10` **响应参数**: ```json { "code": 200, "message": "success", "data": { "total": 2, "list": [ { "id": 1, "workTypes": ["采摘工"], "remark": "日薪300元,...", "keywordFlag": 1, "publisherName": "张三", "createdAt": "2026-05-30 10:00" } ] } } ``` #### PUT /api/admin/audit/recruit-review/{id} — 待复核操作 **请求参数**: ```json { "action": "NORMAL", "reason": "" } ``` 或强制下架: ```json { "action": "FORCE_OFFLINE", "reason": "内容违规" } ``` **业务逻辑**: 1. 验证招工存在 2. NORMAL → keyword_flag=0 3. FORCE_OFFLINE → status=0,记录原因 --- ## 四、安全设计(继承阶段一 + 扩展) ### 4.1 文件上传安全 ``` 前端上传 → 格式白名单校验 → 大小限制 → 存储到本地目录 │ ▼ 返回文件URL ``` - **格式白名单**: 图片(jpg,jpeg,png,gif)、视频(mp4,mov,avi) - **大小限制**: 图片≤5MB,视频≤50MB - **EXIF清除**: 前端责任,后端不二次处理(性能考虑) - **路径安全**: 文件名使用UUID,防止路径穿越 ### 4.2 内容安全 ``` 招工发布 → 敏感词检测 → keyword_flag 标记 → 不阻塞发布 │ ▼ 待复核列表 ``` - **敏感词库**: 初始硬编码,仅含明确违规词 - **检测方式**: 正则匹配 - **处理方式**: keyword_flag=1 进入待复核,不阻塞发布 ### 4.3 拨号安全 ``` 拨号请求 → 记录拨号日志 → 返回脱敏手机号 → 前端拨号 ``` - **日志记录**: 拨号方ID、被拨方ID、时间 - **数据脱敏**: 列表不返回手机号,拨号接口单独获取 --- ## 五、关键技术方案 ### 5.1 距离计算(Haversine公式) ```java public class DistanceUtil { private static final double EARTH_RADIUS = 6371; // 地球半径(km) public static double calculate(double lat1, double lng1, double lat2, double lng2) { double dLat = Math.toRadians(lat2 - lat1); double dLng = Math.toRadians(lng2 - lng1); double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) * Math.sin(dLng / 2) * Math.sin(dLng / 2); double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return EARTH_RADIUS * c; } } ``` **说明**: - 输入:两个经纬度点(DECIMAL(10,7)) - 输出:距离(km,保留1位小数) - 精度:小数点后7位,误差≤1米 ### 5.2 敏感词检测 ```java public class KeywordCheckService { // 初始敏感词库(硬编码) private static final List KEYWORDS = Arrays.asList( // 仅含明确违规词,不含正常业务词汇 ); public boolean containsSensitiveWord(String text) { if (text == null) return false; for (String keyword : KEYWORDS) { if (text.contains(keyword)) return true; } return false; } } ``` ### 5.3 视频压缩(异步) ```java @Service public class VideoCompressService { @Async public void compressVideo(String inputPath, String outputPath) { // FFmpeg 命令:720p,码率2Mbps // ffmpeg -i input.mp4 -vf scale=-1:720 -b:v 2M output.mp4 } } ``` **约束**: - 异步执行,不影响用户操作 - 失败自动重试3次 - 压缩后替换原文件 --- ## 六、阶段2a新增文件清单 | 文件 | 说明 | |------|------| | GrowerProfileController.java | 果农档案API | | GrowerProfileService.java | 果农档案业务逻辑 | | GrowerProfileMapper.xml | 果农档案MyBatis映射 | | RecruitController.java | 招工CRUD API | | RecruitService.java | 招工业务逻辑 | | RecruitMapper.xml | 招工MyBatis映射 | | WorkerSearchController.java | 工人列表API | | WorkerSearchService.java | 工人搜索业务逻辑 | | BuyerSearchController.java | 客商列表API | | BuyerSearchService.java | 客商搜索业务逻辑 | | CallPhoneController.java | 拨号API | | CallLogService.java | 拨号日志业务逻辑 | | CallLogMapper.xml | 拨号日志MyBatis映射 | | GrowerAuditController.java | 名片审核API | | GrowerAuditService.java | 名片审核业务逻辑 | | RecruitReviewController.java | 待复核API | | RecruitReviewService.java | 待复核业务逻辑 | | KeywordCheckService.java | 敏感词检测服务 | | DistanceUtil.java | 距离计算工具类 | | VideoCompressService.java | 视频压缩服务 |