基于 Team A 需求规格(阶段2a spec.md) 新增模块:果农名片、招工发布、找工人、找客商、后台审核
┌─────────────────────────────────────────────────────────────┐
│ 客户端层 │
├─────────────────────────┬───────────────────────────────────┤
│ 微信小程序 (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 哈希索引 │ │
└─────────────────────────┴───────────────────────────────────┘
| 模块 | 包名 | 职责 |
|---|---|---|
| 果农模块 | com.sayu.service | 果农档案CRUD、审核状态管理 |
| 招工模块 | com.sayu.service | 招工发布、编辑、下架、敏感词检测 |
| 搜索模块 | com.sayu.service | 工人列表、客商列表、距离计算 |
| 文件模块 | com.sayu.service | 文件上传、存储、访问、删除 |
| 审核模块(扩展) | com.sayu.service | 名片审核、待复核列表、SLA提醒 |
Controller 层(API 入口)
↓
Service 层(业务逻辑)
↓
Mapper 层(数据访问)
↓
Entity/DTO/VO 层(数据模型)
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'
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| 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) | 驳回原因 |
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| 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 | 发布时间 |
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | BIGINT | PK, AUTO_INCREMENT | 主键 |
| caller_identity_id | BIGINT | INDEX | 拨号方身份ID |
| callee_identity_id | BIGINT | INDEX | 被拨方身份ID |
| call_time | DATETIME | 拨号时间 |
-- 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);
描述: 获取当前果农的名片信息
请求头: Authorization: Bearer {token}
响应参数:
{
"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
}
}
描述: 更新果农名片信息,审核状态重置为待审
请求参数:
{
"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"]
}
业务逻辑:
描述: 发布招工信息,免审核直接发布
请求参数:
{
"workTypes": ["采摘工", "分拣工"],
"price": 150,
"priceUnit": "DAY",
"workerCount": 3,
"days": 5,
"location": "洒渔镇黄兴村",
"latitude": 27.3331,
"longitude": 103.7168,
"remark": "需要有经验的采摘工"
}
业务逻辑:
请求参数: ?page=1&pageSize=20
响应参数:
{
"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"
}
]
}
}
请求参数:
{
"price": 180,
"workerCount": 5,
"remark": "价格上调,急招"
}
业务逻辑:
业务逻辑:
请求参数: ?workType=采摘工&page=1&pageSize=20
响应参数:
{
"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
}
]
}
}
业务逻辑:
请求参数: ?page=1&pageSize=20
响应参数:
{
"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": "昭通市水果批发市场"
}
]
}
}
描述: 获取对方手机号并记录拨号日志
请求参数:
{
"calleeIdentityId": 5
}
响应参数:
{
"code": 200,
"message": "success",
"data": {
"phone": "138****8888",
"realPhone": "13800138000"
}
}
业务逻辑:
请求参数: ?page=1&pageSize=10&status=0
响应参数:
{
"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"
}
]
}
}
请求参数:
{
"action": "APPROVE",
"reason": ""
}
或驳回:
{
"action": "REJECT",
"reason": "视频内容不清晰,请重新上传"
}
业务逻辑:
请求参数: ?page=1&pageSize=10
响应参数:
{
"code": 200,
"message": "success",
"data": {
"total": 2,
"list": [
{
"id": 1,
"workTypes": ["采摘工"],
"remark": "日薪300元,...",
"keywordFlag": 1,
"publisherName": "张三",
"createdAt": "2026-05-30 10:00"
}
]
}
}
请求参数:
{
"action": "NORMAL",
"reason": ""
}
或强制下架:
{
"action": "FORCE_OFFLINE",
"reason": "内容违规"
}
业务逻辑:
前端上传 → 格式白名单校验 → 大小限制 → 存储到本地目录
│
▼
返回文件URL
招工发布 → 敏感词检测 → keyword_flag 标记 → 不阻塞发布
│
▼
待复核列表
拨号请求 → 记录拨号日志 → 返回脱敏手机号 → 前端拨号
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;
}
}
说明:
public class KeywordCheckService {
// 初始敏感词库(硬编码)
private static final List<String> 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;
}
}
@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
}
}
约束:
| 文件 | 说明 |
|---|---|
| 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 | 视频压缩服务 |