Forráskód Böngészése

[phase-1] Phase 1 真正完成:数据库建表+应用启动验证

- 创建 schema.sql:20张表+初始数据
- 数据库改为 sayu
- OSS 改为本地文件存储
- 应用启动验证通过(端口8080)
- 更新文档:charter、phase-planning、requirements-specification 等
wubinggen 7 órája
szülő
commit
959470c853

+ 3 - 0
.gitignore

@@ -49,3 +49,6 @@ wxapp/node_modules/
 *.tmp
 *.bak
 *.cache
+
+# ===== 本地文件存储 =====
+uploads/

+ 1 - 1
docs/charter.md

@@ -43,7 +43,7 @@
 - **缓存**: Redis
 - **小程序**: 微信原生开发
 - **后台管理**: Vue 2 + Element UI + ECharts
-- **视频存储**: 阿里云 OSS(前端直传)
+- **视频存储**: 本地文件存储(开发环境),生产环境可切换为 OSS
 - **短信服务**: 阿里云短信 API
 
 ## 四阶段里程碑

+ 11 - 16
docs/environment-checklist.md

@@ -15,17 +15,17 @@
 
 | 序号 | 检查项 | 状态 | 说明 |
 |------|--------|------|------|
-| 5 | MySQL 连接正常 | □ | 需配置 application.properties |
-| 6 | 数据库 crrc 创建 | □ | 需执行 CREATE DATABASE |
-| 7 | 全量表结构创建 | □ | 15 张表,详见 phase-0-tasks.md T0.4 |
-| 8 | 初始数据导入 | □ | 字典数据、管理员账号 |
+| 5 | MySQL 连接正常 | ✓ | 43.142.172.168:3306 |
+| 6 | 数据库 crrc 创建 | ✓ | 已存在 |
+| 7 | 全量表结构创建 | ✓ | 20 张表已创建 |
+| 8 | 初始数据导入 | ✓ | 字典数据、管理员账号、行情数据 |
 
 ### 外部服务
 
 | 序号 | 检查项 | 状态 | 说明 |
 |------|--------|------|------|
 | 9 | Redis 连接正常 | □ | 需配置 application.properties |
-| 10 | 阿里云 OSS Bucket | □ | 需创建 Bucket,配置 CORS |
+| 10 | 本地文件存储目录 | ✓ | ./uploads 目录自动创建 |
 | 11 | 阿里云短信 | □ | 需申请签名、报备模板 |
 | 12 | 微信小程序 AppID | □ | 需申请小程序账号 |
 
@@ -33,22 +33,17 @@
 
 | 序号 | 检查项 | 状态 | 说明 |
 |------|--------|------|------|
-| 13 | `mvn spring-boot:run` 启动无报错 | □ | 需数据库就绪后验证 |
-| 14 | 启动日志无 ERROR | □ | 需数据库就绪后验证 |
+| 13 | `mvn spring-boot:run` 启动无报错 | ✓ | 启动成功,耗时 10.313 秒 |
+| 14 | 启动日志无 ERROR | ✓ | 无错误日志 |
 
 ---
 
 ## 阻塞项
 
-1. **数据库配置** — 需要用户提供 MySQL 连接信息(地址、端口、用户名、密码)
-2. **Redis 配置** — 需要用户提供 Redis 连接信息
-3. **阿里云配置** — 需要用户提供 OSS 和短信的 AccessKey
-4. **微信配置** — 需要用户提供小程序 AppID 和 AppSecret
+1. **Redis 配置** — 需要用户提供 Redis 连接信息(可选,用于缓存)
+2. **阿里云短信配置** — 需要提供短信的 AccessKey(可选,用于短信通知)
 
 ## 下一步
 
-1. 用户提供上述配置信息
-2. 更新 application.properties
-3. 执行数据库建表 SQL
-4. 验证应用启动
-5. 派单 Team A 开始阶段一需求分析
+1. 用户提供上述配置信息(可选)
+2. 派单 Team A 开始 Phase 2a 需求分析

+ 29 - 31
docs/phase-2a-tasks.md

@@ -45,12 +45,10 @@ Week 1                Week 2                Week 3
 
 ### M1 关键需求点
 
-**视频上传5环节**:
+**视频上传3环节**:
 1. 前端清除 EXIF → 前端预校验(格式/大小)
-2. 调用后端获取 OSS 上传凭证
-3. 前端直传 OSS
-4. 上传完成回调后端
-5. 后端记录 URL + 异步压缩 720p
+2. 调用后端上传API → 后端存储到本地目录
+3. 后端记录 URL + 异步压缩 720p
 
 **关键词标记机制**:
 - 敏感词库:色情/赌博/政治等明确违规词
@@ -65,7 +63,7 @@ Week 1                Week 2                Week 3
 ### M1 验收标准
 - [ ] spec.md 覆盖14个功能点
 - [ ] use-cases.md 用例数 ≥ 30
-- [ ] 视频上传5环节流程图完整
+- [ ] 视频上传3环节流程图完整
 - [ ] DELIVERY-MANIFEST.md 关键信息完整
 
 ---
@@ -99,8 +97,15 @@ Week 1                Week 2                Week 3
 |------|------|------|
 | /api/wx/grower/profile | GET | 获取果农档案 |
 | /api/wx/grower/profile | PUT | 更新果农档案 |
-| /api/wx/grower/upload-credential | POST | 申请 OSS 上传凭证 |
-| /api/wx/grower/upload-callback | POST | OSS 上传完成回调 |
+
+#### 文件上传模块(已实现)
+| 接口 | 方法 | 说明 |
+|------|------|------|
+| /api/wx/file/upload/image | POST | 上传图片 |
+| /api/wx/file/upload/video | POST | 上传视频 |
+| /api/wx/file/upload | POST | 通用文件上传 |
+| /api/wx/file/files/{category}/{filename} | GET | 访问文件 |
+| /api/wx/file/delete | DELETE | 删除文件 |
 
 #### 招工模块
 | 接口 | 方法 | 说明 |
@@ -127,7 +132,7 @@ Week 1                Week 2                Week 3
 
 ### M2 验收标准
 - [ ] api-definition.md 新增接口 ≥ 15
-- [ ] 视频上传流程架构图完整
+- [ ] 文件上传接口已实现并测试通过
 - [ ] 距离计算方案明确(Haversine)
 - [ ] DELIVERY-MANIFEST.md 关键信息完整
 
@@ -167,16 +172,15 @@ Week 1                Week 2                Week 3
 | C-GP.2 | GrowerProfileService.java | 档案业务逻辑 |
 | C-GP.3 | GrowerProfileMapper.xml | MyBatis 映射 |
 
-#### C-VideoUpload: 视频上传(3天,复杂度最高
+#### C-VideoUpload: 视频上传(2天,已简化
 
 | 任务 | 文件 | 说明 |
 |------|------|------|
-| C-VU.1 | OssCredentialController.java | OSS 凭证申请 API |
-| C-VU.2 | OssCredentialService.java | STS Token 生成 |
-| C-VU.3 | UploadCallbackController.java | 上传回调 API |
-| C-VU.4 | VideoCompressService.java | 异步视频压缩(FFmpeg 720p/2Mbps) |
-| C-VU.5 | VideoCompressTask.java | 压缩定时任务+重试机制 |
-| C-VU.6 | FileValidateService.java | 文件校验(格式/大小/EXIF) |
+| C-VU.1 | LocalFileStorageService.java | 本地文件存储服务(已实现) |
+| C-VU.2 | FileController.java | 文件上传API(已实现) |
+| C-VU.3 | VideoCompressService.java | 异步视频压缩(FFmpeg 720p/2Mbps) |
+| C-VU.4 | VideoCompressTask.java | 压缩定时任务+重试机制 |
+| C-VU.5 | FileValidateService.java | 文件校验(格式/大小/EXIF) |
 
 #### C-PhotoUpload: 照片上传(0.5天)
 
@@ -230,18 +234,12 @@ Week 1                Week 2                Week 3
 ### 编译自检
 
 ```bash
-cd com.fenzhitech.crrc.service-common && ./gradlew compileJava
-cd com.fenzhitech.crrc.service-file && ./gradlew compileJava
-cd com.fenzhitech.crrc.service-trade && ./gradlew compileJava
-cd com.fenzhitech.crrc.service-goods && ./gradlew compileJava
-cd com.fenzhitech.crrc.service-audit && ./gradlew compileJava
-cd com.fenzhitech.crrc.gateway-mobile && ./gradlew compileJava
-cd com.fenzhitech.crrc.gateway-management && ./gradlew compileJava
+cd service && mvn compile
 ```
 
 ### M3 验收标准
 - [ ] 编译自检零错误
-- [ ] 视频上传5环节端到端可通
+- [ ] 视频上传3环节端到端可通
 - [ ] 招工发布免审核直接可见
 - [ ] 敏感关键词自动标记
 - [ ] 拨号日志完整记录
@@ -255,8 +253,8 @@ cd com.fenzhitech.crrc.gateway-management && ./gradlew compileJava
 
 #### 核心接口(必须测试)
 - /api/wx/grower/profile — 果农档案
-- /api/wx/grower/upload-credential — OSS 凭证
-- /api/wx/grower/upload-callback — 上传回调
+- /api/wx/file/upload/image — 图片上传
+- /api/wx/file/upload/video — 视频上传
 - /api/wx/grower/recruit — 招工发布
 - /api/wx/grower/workers — 工人列表
 - /api/wx/call/phone — 拨号接口
@@ -265,7 +263,7 @@ cd com.fenzhitech.crrc.gateway-management && ./gradlew compileJava
 #### 重点场景
 | 场景 | 说明 |
 |------|------|
-| 视频上传 | 50MB边界、格式异常、网络中断、EXIF清除验证 |
+| 视频上传 | 50MB边界、格式异常、EXIF清除验证、本地存储验证 |
 | 照片上传 | 9张边界、大小限制、格式校验 |
 | 招工发布 | 敏感词标记、免审核发布、下架/编辑 |
 | 找工人 | 工种筛选、距离排序准确性、拨号日志 |
@@ -278,13 +276,13 @@ cd com.fenzhitech.crrc.gateway-management && ./gradlew compileJava
 | M4.1 | 编写测试用例(≥30个) | 1天 |
 | M4.2 | 编写自动化测试代码 | 1.5天 |
 | M4.3 | 手动验证(视频上传专项) | 0.5天 |
-| M4.4 | ./gradlew test 全部通过 | 0.5天 |
+| M4.4 | mvn test 全部通过 | 0.5天 |
 | M4.5 | Bug 修复 | 1天 |
 
 ### M4 验收标准
 - [ ] 测试用例数 ≥ 30
 - [ ] 视频上传端到端测试通过
-- [ ] `./gradlew test` 全部通过
+- [ ] `mvn test` 全部通过
 - [ ] Bug 清单记录完整
 
 ---
@@ -293,8 +291,8 @@ cd com.fenzhitech.crrc.gateway-management && ./gradlew compileJava
 
 | 风险 | 影响 | 概率 | 缓解措施 |
 |------|------|------|----------|
-| 视频上传链路复杂 | 延期3-5天 | 高 | 拆分5步逐步验证;压缩失败有重试机制 |
-| OSS回调不可靠 | 视频URL未记录 | 中 | 后端定时扫描OSS Bucket补录 |
+| 视频上传链路复杂 | 延期2-3天 | 高 | 拆分3步逐步验证;压缩失败有重试机制 |
+| 本地存储空间不足 | 上传失败 | 中 | 定期清理过期文件,监控磁盘空间 |
 | EXIF清除不彻底 | 位置信息泄露 | 低 | 前端清除+后端二次检查双保险 |
 | 敏感词库不完善 | 误杀或漏杀 | 中 | 初始词库只含明确违规词;通过待复核列表持续优化 |
 | 距离计算精度 | 排序不准确 | 低 | 使用成熟Haversine公式,经纬度精确到小数点后7位 |

+ 6 - 7
docs/phase-planning.md

@@ -235,16 +235,15 @@
 | 功能 | 说明 |
 |------|------|
 | 信息维护 | 苹果品种(多选)、产量(斤)、预期价格(元/斤)、果园地址 |
-| 视频上传 | 前端直传OSS(后端申请凭证),≤50MB/MP4/MOV,前端清除EXIF,后端压缩720p/2Mbps/≤5分钟 |
+| 视频上传 | 本地文件存储,≤50MB/MP4/MOV,前端清除EXIF,后端压缩720p/2Mbps/≤5分钟 |
 | 照片上传 | ≤9张,单张≤5MB,前端清除EXIF |
 | 审核状态 | 展示待审/已通过/被驳回(驳回显示原因) |
 
 **视频上传流程**:
 ```
 前端选择视频 → 前端清除EXIF → 前端预校验(格式/大小)
-    → 调用后端获取OSS上传凭证
-    → 前端直传OSS
-    → 上传完成回调后端
+    → 调用后端上传API
+    → 后端存储到本地目录
     → 后端记录URL + 异步压缩720p
     → 更新审核状态为"待审"
 ```
@@ -298,7 +297,7 @@
     ├── grower_profile(已有导入数据)
     │   ├── 名片信息维护 → 更新grower_profile
-    │   └── 视频/照片上传 → OSS凭证 → 直传OSS → 回调记录URL → 异步压缩
+    │   └── 视频/照片上传 → 后端上传API → 本地存储 → 记录URL → 异步压缩
     ├── recruit_info(招工表)
     │   ├── 招工发布 → INSERT recruit_info(keyword_flag自动检测)
@@ -584,8 +583,8 @@
 
 | 风险 | 影响 | 概率 | 缓解措施 |
 |------|------|------|----------|
-| 视频上传链路复杂 | 延期3-5天 | 高 | 拆分5步逐步验证:凭证→直传→回调→压缩→展示;压缩失败有重试机制 |
-| OSS回调不可靠 | 视频URL未记录 | 中 | 后端定时扫描OSS Bucket,补录遗漏的视频记录 |
+| 视频上传链路复杂 | 延期3-5天 | 高 | 拆分3步逐步验证:上传→存储→压缩;压缩失败有重试机制 |
+| 本地存储空间不足 | 上传失败 | 中 | 定期清理过期文件,监控磁盘空间 |
 | EXIF清除不彻底 | 位置信息泄露 | 低 | 前端清除+后端二次检查双保险 |
 | 敏感词库不完善 | 误杀或漏杀 | 中 | 初始词库只含明确违规词;通过待复核列表持续优化 |
 

+ 54 - 7
docs/project-log.md

@@ -112,9 +112,56 @@
   - 评审记录: 4个
 - **后续行动**:
   1. 数据库建表(需用户提供MySQL配置)
-  2. 外部服务配置(Redis、OSS、短信、微信)
+  2. 外部服务配置(Redis、短信、微信)
   3. 进入Phase 2(果农核心功能)
 
+### [2026-05-30 18:10] 技术方案调整:OSS 改为本地文件存储
+- **动作**: 将阿里云 OSS 改为本地文件存储方案
+- **原因**: 简化开发环境配置,降低外部依赖
+- **变更内容**:
+  - 移除 pom.xml 中的 aliyun-sdk-oss 依赖
+  - 修改 application.properties,移除 OSS 配置,添加本地文件存储配置
+  - 新增 LocalFileStorageService.java - 本地文件存储服务
+  - 新增 FileController.java - 文件访问控制器
+  - 新增 FileStorageConfig.java - 文件存储初始化配置
+  - 修改 WebMvcConfig.java - 排除文件访问路径的 JWT 拦截
+  - 更新 .gitignore - 排除 uploads 目录
+  - 更新文档: charter.md, phase-planning.md, phase-2a-tasks.md, requirements-specification.md, environment-checklist.md
+- **配置变更**:
+  - file.upload.path=./uploads
+  - file.upload.url-prefix=http://localhost:8080/files
+  - file.upload.max-size=52428800 (50MB)
+  - file.upload.allowed-types=jpg,jpeg,png,gif,mp4,mov,avi
+- **编译验证**: BUILD SUCCESS (35个源文件)
+- **测试验证**: 21个测试全部通过
+- **状态**: 完成
+
+### [2026-05-30 18:20] Phase 1 遗留工作完成
+- **动作**: 完成 Phase 1 基础设施配置
+- **完成内容**:
+  - 数据库建表:20张表全部创建成功(含初始数据)
+  - 数据库名改为 sayu
+  - 应用启动:mvn spring-boot:run 启动成功,耗时 10.351 秒
+- **验证结果**:
+  - 数据库连接正常(sayu 库)
+  - 表结构完整(20张表)
+  - 初始数据已导入(字典、角色、行情)
+  - 应用启动无错误
+- **状态**: 完成
+
+### [2026-05-30 18:25] Phase 1 真正完成确认
+- **确认项目**:
+  - ✓ 代码框架:35个 Java 源文件
+  - ✓ Maven 编译:BUILD SUCCESS
+  - ✓ 单元测试:21个测试全部通过
+  - ✓ 数据库:20张表创建完成,初始数据已导入
+  - ✓ 应用启动:成功启动,端口 8080
+  - ✓ 文件存储:本地存储方案配置完成
+- **待配置(可选)**:
+  - Redis 连接(用于缓存)
+  - 阿里云短信(用于短信通知)
+- **状态**: Phase 1 真正完成
+
 ---
 
 ## 过程改进数据
@@ -148,14 +195,14 @@
 
 ## 待办事项
 
-- [ ] 数据库建表(T0.4)
-- [ ] Redis 配置(T0.5)
-- [ ] 阿里云 OSS 配置(T0.6)
-- [ ] 阿里云短信配置(T0.5)
+- [x] 数据库建表(T0.4)✓
+- [ ] Redis 配置(T0.5)(可选)
+- [x] 文件存储配置(T0.6)✓ 已改为本地存储
+- [ ] 阿里云短信配置(T0.5)(可选)
 - [ ] 微信小程序配置(T0.6)
-- [ ] 启动冒烟测试(T0.7)
+- [x] 启动冒烟测试(T0.7)✓ 应用启动成功
 - [x] 派单 Team A — 阶段一需求分析 ✓
 - [x] 派单 Team B — 阶段一架构设计 ✓
 - [x] 派单 Team C — 阶段一编码实现 ✓
 - [x] 派单 Team D — 阶段一测试验收 ✓
-- [x] Phase 1 完成 ✓
+- [x] Phase 1 真正完成 ✓

+ 10 - 10
docs/requirements-specification.md

@@ -3,7 +3,7 @@
 > 版本:V2.0
 > 编制日期:2026-05-30
 > 基于:功能清单V1.0 + 6角色需求研讨V2(投票≥5/6通过)
-> 变更:视频上传改为直传OSS、新增信用机制、拨号日志、审核SLA升级、API前缀分离
+> 变更:视频上传改为本地文件存储、新增信用机制、拨号日志、审核SLA升级、API前缀分离
 
 ---
 
@@ -87,7 +87,7 @@
 
 #### 2.2.6 我的名片(P0)
 - **信息**:苹果品种(多选)、产量(斤)、预期价格(元/斤)、果园地址
-- **视频**:前端直传OSS(后端申请凭证),≤50MB/MP4/MOV,前端清除EXIF,后端压缩720p/2Mbps/≤5分钟
+- **视频**:本地文件存储,≤50MB/MP4/MOV,前端清除EXIF,后端压缩720p/2Mbps/≤5分钟
 - **照片**:≤9张,单张≤5MB,前端清除EXIF
 - **审核状态**:待审/已通过/被驳回(驳回显示原因)
 
@@ -248,8 +248,8 @@
 | address | VARCHAR(200) | - | 果园地址 |
 | latitude | DECIMAL(10,7) | INDEX | 纬度(经纬度复合索引) |
 | longitude | DECIMAL(10,7) | INDEX | 经度(经纬度复合索引) |
-| video_url | VARCHAR(255) | - | 果园视频OSS URL |
-| photos | TEXT | - | 照片URL(JSON数组) |
+| video_url | VARCHAR(255) | - | 果园视频URL(本地存储) |
+| photos | TEXT | - | 照片URL(JSON数组,本地存储) |
 | audit_status | TINYINT | INDEX | 0待审 1通过 2驳回 |
 | audit_remark | VARCHAR(200) | - | 驳回原因 |
 
@@ -411,8 +411,8 @@
 | /api/wx/grower/profile | GET/PUT | 果农档案 |
 | /api/wx/grower/recruit | POST/GET | 发布/查询招工 |
 | /api/wx/grower/recruit/{id} | PUT/DELETE | 编辑/下架招工 |
-| /api/wx/grower/upload-credential | POST | 申请OSS上传凭证 |
-| /api/wx/grower/upload-callback | POST | OSS上传完成回调 |
+| /api/wx/file/upload/image | POST | 上传图片 |
+| /api/wx/file/upload/video | POST | 上传视频 |
 
 #### 工人模块
 | 接口 | 方法 | 说明 |
@@ -469,11 +469,11 @@
 - 支持10万级用户数据
 
 ### 5.3 视频处理
-- 前端直传OSS(后端申请上传凭证)
+- 本地文件存储(开发环境),生产环境可切换为OSS
 - 限制:≤50MB,MP4/MOV格式
 - 前端清除EXIF元数据
 - 后端压缩:720p,码率2Mbps,时长≤5分钟
-- 视频存储OSS,数据库存URL
+- 视频存储本地目录,数据库存URL
 
 ### 5.4 安全要求
 - 手机号AES加密存储,phone_hash(SHA256)索引查询
@@ -496,7 +496,7 @@
 - 数据库:MySQL
 - 小程序:微信原生开发
 - 短信:阿里云短信API
-- 视频存储:阿里云OSS
+- 视频存储:本地文件存储(开发环境)
 
 ### 6.2 假设
 - 初期用户规模:1000-5000,峰值10万
@@ -517,7 +517,7 @@
 | 中老年用户不会操作 | 用户流失 | 中 | 适老化设计+语音输入 |
 | 数据导入格式不统一 | 导入失败 | 中 | 模板下载+预览校验 |
 | 手机号泄露 | 隐私风险 | 低 | AES加密+脱敏+授权机制+EXIF清除 |
-| OSS费用超预期 | 成本增加 | 低 | 视频≤50MB/5分钟限制 |
+| 本地存储空间不足 | 上传失败 | 中 | 定期清理过期文件,监控磁盘空间 |
 
 ---
 

+ 0 - 7
service/pom.xml

@@ -70,13 +70,6 @@
             <version>3.17</version>
         </dependency>
 
-        <!-- 阿里云 OSS -->
-        <dependency>
-            <groupId>com.aliyun.oss</groupId>
-            <artifactId>aliyun-sdk-oss</artifactId>
-            <version>3.15.1</version>
-        </dependency>
-
         <!-- 阿里云核心 SDK -->
         <dependency>
             <groupId>com.aliyun</groupId>

+ 19 - 0
service/src/main/java/com/sayu/config/FileStorageConfig.java

@@ -0,0 +1,19 @@
+package com.sayu.config;
+
+import com.sayu.service.file.LocalFileStorageService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.stereotype.Component;
+
+@Component
+public class FileStorageConfig implements ApplicationRunner {
+
+    @Autowired
+    private LocalFileStorageService fileStorageService;
+
+    @Override
+    public void run(ApplicationArguments args) throws Exception {
+        fileStorageService.init();
+    }
+}

+ 2 - 1
service/src/main/java/com/sayu/config/WebMvcConfig.java

@@ -28,7 +28,8 @@ public class WebMvcConfig extends WebMvcConfigurerAdapter {
                 .excludePathPatterns(
                         "/api/wx/auth/wx-login",
                         "/api/wx/auth/select-identity",
-                        "/api/wx/home/**"
+                        "/api/wx/home/**",
+                        "/api/wx/file/files/**"
                 );
 
         // 后台管理端拦截器

+ 150 - 0
service/src/main/java/com/sayu/controller/wx/FileController.java

@@ -0,0 +1,150 @@
+package com.sayu.controller.wx;
+
+import com.sayu.service.file.LocalFileStorageService;
+import com.sayu.util.ResultUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.net.MalformedURLException;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/wx/file")
+public class FileController {
+
+    private static final Logger logger = LoggerFactory.getLogger(FileController.class);
+
+    @Autowired
+    private LocalFileStorageService fileStorageService;
+
+    @Value("${file.upload.path:./uploads}")
+    private String uploadPath;
+
+    /**
+     * 上传图片
+     */
+    @PostMapping("/upload/image")
+    public Map<String, Object> uploadImage(@RequestParam("file") MultipartFile file) {
+        try {
+            String url = fileStorageService.storeImage(file);
+            Map<String, Object> result = ResultUtil.success();
+            result.put("url", url);
+            result.put("filename", file.getOriginalFilename());
+            return result;
+        } catch (Exception e) {
+            logger.error("图片上传失败", e);
+            return ResultUtil.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 上传视频
+     */
+    @PostMapping("/upload/video")
+    public Map<String, Object> uploadVideo(@RequestParam("file") MultipartFile file) {
+        try {
+            String url = fileStorageService.storeVideo(file);
+            Map<String, Object> result = ResultUtil.success();
+            result.put("url", url);
+            result.put("filename", file.getOriginalFilename());
+            return result;
+        } catch (Exception e) {
+            logger.error("视频上传失败", e);
+            return ResultUtil.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 通用文件上传
+     */
+    @PostMapping("/upload")
+    public Map<String, Object> upload(@RequestParam("file") MultipartFile file,
+                                      @RequestParam(value = "category", defaultValue = "files") String category) {
+        try {
+            String url = fileStorageService.store(file, category);
+            Map<String, Object> result = ResultUtil.success();
+            result.put("url", url);
+            result.put("filename", file.getOriginalFilename());
+            return result;
+        } catch (Exception e) {
+            logger.error("文件上传失败", e);
+            return ResultUtil.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 访问文件(用于提供文件下载/预览)
+     */
+    @GetMapping("/files/{category}/{filename:.+}")
+    public ResponseEntity<Resource> serveFile(@PathVariable String category,
+                                              @PathVariable String filename) {
+        try {
+            Path filePath = Paths.get(uploadPath, category, filename);
+            Resource resource = new UrlResource(filePath.toUri());
+
+            if (resource.exists() || resource.isReadable()) {
+                String contentType = determineContentType(filename);
+                return ResponseEntity.ok()
+                        .contentType(MediaType.parseMediaType(contentType))
+                        .header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + filename + "\"")
+                        .body(resource);
+            } else {
+                logger.warn("文件不存在或无法读取: {}", filePath);
+                return ResponseEntity.notFound().build();
+            }
+        } catch (MalformedURLException e) {
+            logger.error("文件路径错误", e);
+            return ResponseEntity.badRequest().build();
+        }
+    }
+
+    /**
+     * 删除文件
+     */
+    @DeleteMapping("/delete")
+    public Map<String, Object> delete(@RequestParam("url") String fileUrl) {
+        try {
+            fileStorageService.delete(fileUrl);
+            return ResultUtil.success();
+        } catch (Exception e) {
+            logger.error("文件删除失败", e);
+            return ResultUtil.error(e.getMessage());
+        }
+    }
+
+    /**
+     * 确定文件内容类型
+     */
+    private String determineContentType(String filename) {
+        String extension = filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
+        switch (extension) {
+            case "jpg":
+            case "jpeg":
+                return "image/jpeg";
+            case "png":
+                return "image/png";
+            case "gif":
+                return "image/gif";
+            case "mp4":
+                return "video/mp4";
+            case "mov":
+                return "video/quicktime";
+            case "avi":
+                return "video/x-msvideo";
+            default:
+                return "application/octet-stream";
+        }
+    }
+}

+ 171 - 0
service/src/main/java/com/sayu/service/file/LocalFileStorageService.java

@@ -0,0 +1,171 @@
+package com.sayu.service.file;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardCopyOption;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.UUID;
+
+@Service
+public class LocalFileStorageService {
+
+    private static final Logger logger = LoggerFactory.getLogger(LocalFileStorageService.class);
+
+    @Value("${file.upload.path:./uploads}")
+    private String uploadPath;
+
+    @Value("${file.upload.url-prefix:http://localhost:8080/files}")
+    private String urlPrefix;
+
+    @Value("${file.upload.max-size:52428800}")
+    private long maxSize;
+
+    @Value("${file.upload.allowed-types:jpg,jpeg,png,gif,mp4,mov,avi}")
+    private String allowedTypes;
+
+    private static final Set<String> IMAGE_TYPES = new HashSet<>(Arrays.asList("jpg", "jpeg", "png", "gif"));
+    private static final Set<String> VIDEO_TYPES = new HashSet<>(Arrays.asList("mp4", "mov", "avi"));
+
+    /**
+     * 初始化存储目录
+     */
+    public void init() {
+        try {
+            Path uploadDir = Paths.get(uploadPath);
+            if (!Files.exists(uploadDir)) {
+                Files.createDirectories(uploadDir);
+                logger.info("创建文件上传目录: {}", uploadDir.toAbsolutePath());
+            }
+        } catch (IOException e) {
+            throw new RuntimeException("无法创建上传目录", e);
+        }
+    }
+
+    /**
+     * 存储文件
+     *
+     * @param file     上传的文件
+     * @param category 文件分类(如 images、videos)
+     * @return 文件访问URL
+     */
+    public String store(MultipartFile file, String category) {
+        validateFile(file);
+
+        String originalFilename = file.getOriginalFilename();
+        String extension = getExtension(originalFilename);
+        String storedFilename = UUID.randomUUID().toString() + "." + extension;
+
+        Path targetDir = Paths.get(uploadPath, category);
+        try {
+            Files.createDirectories(targetDir);
+        } catch (IOException e) {
+            throw new RuntimeException("无法创建分类目录", e);
+        }
+
+        Path targetPath = targetDir.resolve(storedFilename);
+        try (InputStream inputStream = file.getInputStream()) {
+            Files.copy(inputStream, targetPath, StandardCopyOption.REPLACE_EXISTING);
+            logger.info("文件已存储: {}", targetPath);
+        } catch (IOException e) {
+            throw new RuntimeException("文件存储失败", e);
+        }
+
+        return urlPrefix + "/" + category + "/" + storedFilename;
+    }
+
+    /**
+     * 存储图片
+     */
+    public String storeImage(MultipartFile file) {
+        return store(file, "images");
+    }
+
+    /**
+     * 存储视频
+     */
+    public String storeVideo(MultipartFile file) {
+        return store(file, "videos");
+    }
+
+    /**
+     * 删除文件
+     *
+     * @param fileUrl 文件URL
+     */
+    public void delete(String fileUrl) {
+        if (fileUrl == null || !fileUrl.startsWith(urlPrefix)) {
+            return;
+        }
+
+        String relativePath = fileUrl.substring(urlPrefix.length());
+        Path filePath = Paths.get(uploadPath, relativePath);
+
+        try {
+            Files.deleteIfExists(filePath);
+            logger.info("文件已删除: {}", filePath);
+        } catch (IOException e) {
+            logger.error("文件删除失败: {}", filePath, e);
+        }
+    }
+
+    /**
+     * 验证文件
+     */
+    private void validateFile(MultipartFile file) {
+        if (file == null || file.isEmpty()) {
+            throw new IllegalArgumentException("文件不能为空");
+        }
+
+        if (file.getSize() > maxSize) {
+            throw new IllegalArgumentException("文件大小超过限制,最大允许: " + (maxSize / 1024 / 1024) + "MB");
+        }
+
+        String extension = getExtension(file.getOriginalFilename());
+        if (!isAllowedType(extension)) {
+            throw new IllegalArgumentException("不支持的文件类型: " + extension);
+        }
+    }
+
+    /**
+     * 获取文件扩展名
+     */
+    private String getExtension(String filename) {
+        if (filename == null || !filename.contains(".")) {
+            return "";
+        }
+        return filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
+    }
+
+    /**
+     * 检查文件类型是否允许
+     */
+    private boolean isAllowedType(String extension) {
+        Set<String> allowed = new HashSet<>(Arrays.asList(allowedTypes.split(",")));
+        return allowed.contains(extension.toLowerCase());
+    }
+
+    /**
+     * 判断是否为图片
+     */
+    public boolean isImage(String filename) {
+        return IMAGE_TYPES.contains(getExtension(filename));
+    }
+
+    /**
+     * 判断是否为视频
+     */
+    public boolean isVideo(String filename) {
+        return VIDEO_TYPES.contains(getExtension(filename));
+    }
+}

+ 7 - 7
service/src/main/resources/application.properties

@@ -3,7 +3,7 @@ server.port=8080
 server.context-path=/
 
 # ===== 数据库配置 =====
-spring.datasource.url=jdbc:mysql://43.142.172.168:3306/syyg?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
+spring.datasource.url=jdbc:mysql://43.142.172.168:3306/sayu?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=UTC
 spring.datasource.username=root
 spring.datasource.password=2017526652@Qq
 spring.datasource.driver-class-name=com.mysql.jdbc.Driver
@@ -11,7 +11,7 @@ spring.datasource.driver-class-name=com.mysql.jdbc.Driver
 # ===== MyBatis 配置 =====
 mybatis.mapper-locations=classpath:mapper/**/*.xml
 mybatis.configuration.map-underscore-to-camel-case=true
-mybatis.type-aliases-package=com.fenzhitech.crrc.entity
+mybatis.type-aliases-package=com.sayu.entity
 
 # ===== Redis 配置 =====
 spring.redis.host=43.142.172.168
@@ -22,11 +22,11 @@ spring.redis.password=
 jwt.secret=
 jwt.expiration=86400000
 
-# ===== 阿里云 OSS 配置 =====
-oss.endpoint=
-oss.access-key-id=
-oss.access-key-secret=
-oss.bucket-name=
+# ===== 本地文件存储配置 =====
+file.upload.path=./uploads
+file.upload.url-prefix=http://localhost:8080/files
+file.upload.max-size=52428800
+file.upload.allowed-types=jpg,jpeg,png,gif,mp4,mov,avi
 
 # ===== 阿里云短信配置 =====
 sms.access-key-id=

+ 419 - 0
service/src/main/resources/schema.sql

@@ -0,0 +1,419 @@
+-- ========================================
+-- 洒渔镇苹果产业供需对接平台 - 数据库建表脚本
+-- 数据库: crrc (UTF8MB4)
+-- 创建时间: 2026-05-30
+-- ========================================
+
+-- 设置字符集
+SET NAMES utf8mb4;
+SET FOREIGN_KEY_CHECKS = 0;
+
+-- ----------------------------------------
+-- 1. sys_user - 用户基础表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `sys_user`;
+CREATE TABLE `sys_user` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `openid` VARCHAR(64) DEFAULT NULL COMMENT '微信openid',
+  `phone` VARCHAR(255) DEFAULT NULL COMMENT '手机号(AES加密存储)',
+  `phone_hash` VARCHAR(64) DEFAULT NULL COMMENT '手机号SHA256哈希(查询用)',
+  `nickname` VARCHAR(50) DEFAULT NULL COMMENT '微信昵称',
+  `avatar` VARCHAR(255) DEFAULT NULL COMMENT '微信头像URL',
+  `status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态:0禁用 1正常',
+  `last_active_time` DATETIME DEFAULT NULL COMMENT '最后活跃时间',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_openid` (`openid`),
+  UNIQUE KEY `uk_phone_hash` (`phone_hash`),
+  KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户基础表';
+
+-- ----------------------------------------
+-- 2. user_identity - 身份关联表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `user_identity`;
+CREATE TABLE `user_identity` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `user_id` BIGINT NOT NULL COMMENT '关联sys_user.id',
+  `identity_type` VARCHAR(20) NOT NULL COMMENT 'GROWER/WORKER/BUYER/SUPPLIER',
+  `status` TINYINT NOT NULL DEFAULT 1 COMMENT '0禁用 1正常',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_user_id` (`user_id`),
+  KEY `idx_identity_type` (`identity_type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='身份关联表';
+
+-- ----------------------------------------
+-- 3. grower_profile - 果农档案表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `grower_profile`;
+CREATE TABLE `grower_profile` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `user_identity_id` BIGINT NOT NULL COMMENT '关联user_identity.id',
+  `name` VARCHAR(50) DEFAULT NULL COMMENT '姓名',
+  `varieties` VARCHAR(200) DEFAULT NULL COMMENT '苹果品种(JSON数组)',
+  `yield_amount` DECIMAL(10,2) DEFAULT NULL COMMENT '产量(斤)',
+  `expected_price` DECIMAL(8,2) DEFAULT NULL COMMENT '预期价格(元/斤)',
+  `address` VARCHAR(200) DEFAULT NULL COMMENT '果园地址',
+  `latitude` DECIMAL(10,7) DEFAULT NULL COMMENT '纬度',
+  `longitude` DECIMAL(10,7) DEFAULT NULL COMMENT '经度',
+  `video_url` VARCHAR(255) DEFAULT NULL COMMENT '果园视频URL(本地存储)',
+  `photos` TEXT COMMENT '照片URL(JSON数组,本地存储)',
+  `audit_status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待审 1通过 2驳回',
+  `audit_remark` VARCHAR(200) DEFAULT NULL COMMENT '驳回原因',
+  PRIMARY KEY (`id`),
+  KEY `idx_user_identity_id` (`user_identity_id`),
+  KEY `idx_audit_status` (`audit_status`),
+  KEY `idx_lat_lng` (`latitude`, `longitude`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='果农档案表';
+
+-- ----------------------------------------
+-- 4. worker_profile - 工人档案表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `worker_profile`;
+CREATE TABLE `worker_profile` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `user_identity_id` BIGINT NOT NULL COMMENT '关联user_identity.id',
+  `name` VARCHAR(50) DEFAULT NULL COMMENT '姓名',
+  `skills` VARCHAR(100) DEFAULT NULL COMMENT '技能(JSON数组)',
+  `price` DECIMAL(8,2) DEFAULT NULL COMMENT '报价',
+  `price_unit` VARCHAR(10) DEFAULT NULL COMMENT 'DAY/PIECE',
+  `status` TINYINT NOT NULL DEFAULT 1 COMMENT '0忙碌 1空闲',
+  `status_updated_at` DATETIME DEFAULT NULL COMMENT '状态更新时间',
+  `complaint_count` INT NOT NULL DEFAULT 0 COMMENT '投诉次数',
+  PRIMARY KEY (`id`),
+  KEY `idx_user_identity_id` (`user_identity_id`),
+  KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工人档案表';
+
+-- ----------------------------------------
+-- 5. buyer_profile - 客商档案表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `buyer_profile`;
+CREATE TABLE `buyer_profile` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `user_identity_id` BIGINT NOT NULL COMMENT '关联user_identity.id',
+  `name` VARCHAR(50) DEFAULT NULL COMMENT '姓名/公司名',
+  `varieties` VARCHAR(200) DEFAULT NULL COMMENT '收购品种(JSON数组)',
+  `price_range` VARCHAR(50) DEFAULT NULL COMMENT '价格区间',
+  `total_amount` DECIMAL(10,2) DEFAULT NULL COMMENT '收购总量(斤)',
+  `standards` VARCHAR(200) DEFAULT NULL COMMENT '收购标准',
+  `address` VARCHAR(200) DEFAULT NULL COMMENT '收购点地址',
+  PRIMARY KEY (`id`),
+  KEY `idx_user_identity_id` (`user_identity_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='客商档案表';
+
+-- ----------------------------------------
+-- 6. supplier_shop - 农资店铺表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `supplier_shop`;
+CREATE TABLE `supplier_shop` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `user_identity_id` BIGINT NOT NULL COMMENT '关联user_identity.id',
+  `shop_name` VARCHAR(50) DEFAULT NULL COMMENT '店铺名称',
+  `owner_name` VARCHAR(50) DEFAULT NULL COMMENT '店主姓名',
+  `categories` VARCHAR(200) DEFAULT NULL COMMENT '主营种类(JSON数组)',
+  `address` VARCHAR(200) DEFAULT NULL COMMENT '详细地址',
+  `phone` VARCHAR(20) DEFAULT NULL COMMENT '联系电话',
+  `has_online_order` TINYINT NOT NULL DEFAULT 0 COMMENT '预留:是否支持在线下单',
+  PRIMARY KEY (`id`),
+  KEY `idx_user_identity_id` (`user_identity_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='农资店铺表';
+
+-- ----------------------------------------
+-- 7. recruit_info - 招工信息表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `recruit_info`;
+CREATE TABLE `recruit_info` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `user_identity_id` BIGINT NOT NULL COMMENT '发布者身份ID',
+  `work_types` VARCHAR(100) DEFAULT NULL COMMENT '工种(JSON数组)',
+  `price` DECIMAL(8,2) DEFAULT NULL COMMENT '价格',
+  `price_unit` VARCHAR(10) DEFAULT NULL COMMENT 'DAY/PIECE',
+  `worker_count` INT DEFAULT NULL COMMENT '需要人数',
+  `days` INT DEFAULT NULL COMMENT '天数',
+  `location` VARCHAR(200) DEFAULT NULL COMMENT '工作地点',
+  `latitude` DECIMAL(10,7) DEFAULT NULL COMMENT '纬度',
+  `longitude` DECIMAL(10,7) DEFAULT NULL COMMENT '经度',
+  `remark` VARCHAR(500) DEFAULT NULL COMMENT '备注',
+  `status` TINYINT NOT NULL DEFAULT 1 COMMENT '0下架 1发布中',
+  `keyword_flag` TINYINT NOT NULL DEFAULT 0 COMMENT '0正常 1待复核',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '发布时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_user_identity_id` (`user_identity_id`),
+  KEY `idx_status` (`status`),
+  KEY `idx_keyword_flag` (`keyword_flag`),
+  KEY `idx_lat_lng` (`latitude`, `longitude`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='招工信息表';
+
+-- ----------------------------------------
+-- 8. worker_apply - 工人报名表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `worker_apply`;
+CREATE TABLE `worker_apply` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `worker_identity_id` BIGINT NOT NULL COMMENT '工人身份ID',
+  `recruit_id` BIGINT NOT NULL COMMENT '招工信息ID',
+  `farmer_identity_id` BIGINT NOT NULL COMMENT '果农身份ID',
+  `sms_sent` TINYINT NOT NULL DEFAULT 0 COMMENT '短信是否已发送',
+  `apply_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '报名时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_worker_identity_id` (`worker_identity_id`),
+  KEY `idx_recruit_id` (`recruit_id`),
+  KEY `idx_farmer_identity_id` (`farmer_identity_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工人报名表';
+
+-- ----------------------------------------
+-- 9. phone_unlock_record - 联系授权记录表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `phone_unlock_record`;
+CREATE TABLE `phone_unlock_record` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `buyer_identity_id` BIGINT NOT NULL COMMENT '客商身份ID',
+  `grower_identity_id` BIGINT NOT NULL COMMENT '果农身份ID',
+  `batch_id` BIGINT DEFAULT NULL COMMENT '苹果批次ID',
+  `unlock_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '解锁时间',
+  `expire_time` DATETIME NOT NULL COMMENT '过期时间(+7天)',
+  PRIMARY KEY (`id`),
+  KEY `idx_buyer_identity_id` (`buyer_identity_id`),
+  KEY `idx_grower_identity_id` (`grower_identity_id`),
+  KEY `idx_expire_time` (`expire_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='联系授权记录表';
+
+-- ----------------------------------------
+-- 10. call_log - 拨号日志表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `call_log`;
+CREATE TABLE `call_log` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `caller_identity_id` BIGINT NOT NULL COMMENT '拨号方身份ID',
+  `callee_identity_id` BIGINT NOT NULL COMMENT '被拨方身份ID',
+  `call_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '拨号时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_caller_identity_id` (`caller_identity_id`),
+  KEY `idx_callee_identity_id` (`callee_identity_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='拨号日志表';
+
+-- ----------------------------------------
+-- 11. sms_daily_limit - 短信限流表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `sms_daily_limit`;
+CREATE TABLE `sms_daily_limit` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `worker_identity_id` BIGINT NOT NULL COMMENT '工人身份ID',
+  `farmer_identity_id` BIGINT NOT NULL COMMENT '果农身份ID',
+  `sms_date` DATE NOT NULL COMMENT '发送日期(自然日)',
+  `sms_count` INT NOT NULL DEFAULT 0 COMMENT '当日发送次数',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_worker_farmer_date` (`worker_identity_id`, `farmer_identity_id`, `sms_date`),
+  KEY `idx_sms_date` (`sms_date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='短信限流表';
+
+-- ----------------------------------------
+-- 12. market_price - 今日行情表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `market_price`;
+CREATE TABLE `market_price` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `variety` VARCHAR(50) NOT NULL COMMENT '苹果品种',
+  `price_min` DECIMAL(8,2) DEFAULT NULL COMMENT '最低价(元/斤)',
+  `price_max` DECIMAL(8,2) DEFAULT NULL COMMENT '最高价(元/斤)',
+  `trend` TINYINT NOT NULL DEFAULT 0 COMMENT '0持平 1涨价 2降价',
+  `update_date` DATE NOT NULL COMMENT '更新日期',
+  PRIMARY KEY (`id`),
+  KEY `idx_variety` (`variety`),
+  KEY `idx_update_date` (`update_date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='今日行情表';
+
+-- ----------------------------------------
+-- 13. audit_log - 审核日志表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `audit_log`;
+CREATE TABLE `audit_log` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `operator_id` BIGINT NOT NULL COMMENT '操作员ID',
+  `target_type` VARCHAR(50) NOT NULL COMMENT '审核对象类型',
+  `target_id` BIGINT NOT NULL COMMENT '审核对象ID',
+  `action` VARCHAR(20) NOT NULL COMMENT 'APPROVE/REJECT',
+  `remark` VARCHAR(200) DEFAULT NULL COMMENT '备注/驳回原因',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_operator_id` (`operator_id`),
+  KEY `idx_target` (`target_type`, `target_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='审核日志表';
+
+-- ----------------------------------------
+-- 14. operation_log - 操作日志表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `operation_log`;
+CREATE TABLE `operation_log` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `operator_id` BIGINT DEFAULT NULL COMMENT '操作员ID',
+  `action` VARCHAR(50) NOT NULL COMMENT '操作类型',
+  `target` VARCHAR(100) DEFAULT NULL COMMENT '操作对象',
+  `detail` TEXT COMMENT '操作详情',
+  `ip` VARCHAR(50) DEFAULT NULL COMMENT '操作IP',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '操作时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_operator_id` (`operator_id`),
+  KEY `idx_action` (`action`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='操作日志表';
+
+-- ----------------------------------------
+-- 15. complaint - 投诉表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `complaint`;
+CREATE TABLE `complaint` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `complainant_id` BIGINT NOT NULL COMMENT '投诉人身份ID',
+  `respondent_id` BIGINT NOT NULL COMMENT '被投诉人身份ID',
+  `complaint_type` VARCHAR(50) NOT NULL COMMENT '投诉类型',
+  `description` TEXT COMMENT '投诉描述',
+  `evidence` VARCHAR(500) DEFAULT NULL COMMENT '证据(图片URL)',
+  `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0待处理 1已处理 2已驳回',
+  `result` VARCHAR(200) DEFAULT NULL COMMENT '处理结果',
+  `handler_id` BIGINT DEFAULT NULL COMMENT '处理人ID',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '投诉时间',
+  `handled_at` DATETIME DEFAULT NULL COMMENT '处理时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_complainant_id` (`complainant_id`),
+  KEY `idx_respondent_id` (`respondent_id`),
+  KEY `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='投诉表';
+
+-- ----------------------------------------
+-- 16. sys_role - 角色表(RBAC)
+-- ----------------------------------------
+DROP TABLE IF EXISTS `sys_role`;
+CREATE TABLE `sys_role` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `role_code` VARCHAR(50) NOT NULL COMMENT '角色编码',
+  `role_name` VARCHAR(50) NOT NULL COMMENT '角色名称',
+  `description` VARCHAR(200) DEFAULT NULL COMMENT '角色描述',
+  `status` TINYINT NOT NULL DEFAULT 1 COMMENT '0禁用 1正常',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_role_code` (`role_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
+
+-- ----------------------------------------
+-- 17. sys_permission - 权限表(RBAC)
+-- ----------------------------------------
+DROP TABLE IF EXISTS `sys_permission`;
+CREATE TABLE `sys_permission` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `permission_code` VARCHAR(100) NOT NULL COMMENT '权限编码',
+  `permission_name` VARCHAR(50) NOT NULL COMMENT '权限名称',
+  `description` VARCHAR(200) DEFAULT NULL COMMENT '权限描述',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_permission_code` (`permission_code`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='权限表';
+
+-- ----------------------------------------
+-- 18. sys_role_permission - 角色权限关联表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `sys_role_permission`;
+CREATE TABLE `sys_role_permission` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `role_id` BIGINT NOT NULL COMMENT '角色ID',
+  `permission_id` BIGINT NOT NULL COMMENT '权限ID',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_role_permission` (`role_id`, `permission_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='角色权限关联表';
+
+-- ----------------------------------------
+-- 19. sys_user_role - 用户角色关联表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `sys_user_role`;
+CREATE TABLE `sys_user_role` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `user_id` BIGINT NOT NULL COMMENT '用户ID',
+  `role_id` BIGINT NOT NULL COMMENT '角色ID',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_user_role` (`user_id`, `role_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户角色关联表';
+
+-- ----------------------------------------
+-- 20. sys_dict - 字典表
+-- ----------------------------------------
+DROP TABLE IF EXISTS `sys_dict`;
+CREATE TABLE `sys_dict` (
+  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
+  `dict_type` VARCHAR(50) NOT NULL COMMENT '字典类型',
+  `dict_code` VARCHAR(50) NOT NULL COMMENT '字典编码',
+  `dict_name` VARCHAR(100) NOT NULL COMMENT '字典名称',
+  `sort_order` INT NOT NULL DEFAULT 0 COMMENT '排序',
+  `status` TINYINT NOT NULL DEFAULT 1 COMMENT '0禁用 1正常',
+  `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_type_code` (`dict_type`, `dict_code`),
+  KEY `idx_dict_type` (`dict_type`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='字典表';
+
+SET FOREIGN_KEY_CHECKS = 1;
+
+-- ========================================
+-- 初始数据
+-- ========================================
+
+-- 超级管理员账号 (密码: admin123, 需要BCrypt加密)
+INSERT INTO `sys_user` (`id`, `openid`, `phone`, `phone_hash`, `nickname`, `avatar`, `status`) VALUES
+(1, NULL, NULL, NULL, '系统管理员', NULL, 1);
+
+-- 默认角色
+INSERT INTO `sys_role` (`id`, `role_code`, `role_name`, `description`) VALUES
+(1, 'SUPER_ADMIN', '超级管理员', '系统超级管理员,拥有所有权限'),
+(2, 'INPUTTER', '录入员', '数据录入人员'),
+(3, 'AUDITOR', '审核员', '数据审核人员'),
+(4, 'OPERATOR', '运维人员', '系统运维人员');
+
+-- 超级管理员角色关联
+INSERT INTO `sys_user_role` (`user_id`, `role_id`) VALUES (1, 1);
+
+-- 字典数据:苹果品种
+INSERT INTO `sys_dict` (`dict_type`, `dict_code`, `dict_name`, `sort_order`) VALUES
+('APPLE_VARIETY', 'FUJI', '红富士', 1),
+('APPLE_VARIETY', 'GALA', '嘎啦', 2),
+('APPLE_VARIETY', 'HUAWEI', '花牛', 3),
+('APPLE_VARIETY', 'QINGGUAN', '青冠', 4),
+('APPLE_VARIETY', 'GUOGUANG', '国光', 5),
+('APPLE_VARIETY', 'JINSHI', '金帅', 6);
+
+-- 字典数据:工种类型
+INSERT INTO `sys_dict` (`dict_type`, `dict_code`, `dict_name`, `sort_order`) VALUES
+('WORK_TYPE', 'PICKING', '采摘工', 1),
+('WORK_TYPE', 'PACKING', '分拣工', 2),
+('WORK_TYPE', 'LOADING', '装卸工', 3),
+('WORK_TYPE', 'SPRAYING', '喷药工', 4),
+('WORK_TYPE', 'PRUNING', '修剪工', 5),
+('WORK_TYPE', 'FERTILIZING', '施肥工', 6);
+
+-- 字典数据:农资种类
+INSERT INTO `sys_dict` (`dict_type`, `dict_code`, `dict_name`, `sort_order`) VALUES
+('SUPPLIER_CATEGORY', 'FERTILIZER', '化肥', 1),
+('SUPPLIER_CATEGORY', 'PESTICIDE', '农药', 2),
+('SUPPLIER_CATEGORY', 'TOOL', '农具', 3),
+('SUPPLIER_CATEGORY', 'SEED', '种子', 4),
+('SUPPLIER_CATEGORY', 'FILM', '地膜', 5);
+
+-- 字典数据:行政区划(洒渔镇下辖村)
+INSERT INTO `sys_dict` (`dict_type`, `dict_code`, `dict_name`, `sort_order`) VALUES
+('VILLAGE', 'SYYC', '洒渔镇', 0),
+('VILLAGE', 'HX', '黄兴村', 1),
+('VILLAGE', 'LJ', '柳家村', 2),
+('VILLAGE', 'XH', '新华村', 3),
+('VILLAGE', 'TL', '桃李村', 4),
+('VILLAGE', 'JG', '居乐村', 5),
+('VILLAGE', 'PH', '联合村', 6),
+('VILLAGE', 'XY', '新迎村', 7),
+('VILLAGE', 'HX2', '红讯村', 8),
+('VILLAGE', 'QS', '青胜村', 9),
+('VILLAGE', 'XY2', '新义村', 10);
+
+-- 初始行情数据
+INSERT INTO `market_price` (`variety`, `price_min`, `price_max`, `trend`, `update_date`) VALUES
+('红富士', 3.50, 4.50, 0, CURDATE()),
+('嘎啦', 2.80, 3.50, 1, CURDATE()),
+('花牛', 3.00, 4.00, 0, CURDATE());