diff --git a/docs/expiry/API.md b/docs/expiry/API.md new file mode 100644 index 0000000..d84d711 --- /dev/null +++ b/docs/expiry/API.md @@ -0,0 +1,396 @@ +# 保质期提醒小程序 - API 文档 + +## 基础信息 + +**Base URL**: `/api/expiry` + +**认证方式**: JWT Token(通过公共登录接口获取,参见 `docs/common/auth.md`) + +**请求头**: +``` +Authorization: Bearer +Content-Type: application/json +``` + +--- + +## 1. 物品管理 + +### 1.1 获取物品列表 + +**接口**: `GET /api/expiry/items` + +**Query 参数**: + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| status | string | ❌ | 状态筛选:all/expiring/expired/normal/used | +| category | string | ❌ | 分类筛选:all/food/medicine/cosmetic/other | +| sort | string | ❌ | 排序方式:expiry_date/created_at,默认 expiry_date | +| page | int | ❌ | 页码,默认 1 | +| page_size | int | ❌ | 每页数量,默认 20,最大 100 | + +**响应示例**: +```json +{ + "code": 0, + "message": "success", + "data": { + "items": [ + { + "id": 1, + "name": "牛奶", + "category": "food", + "production_date": "2026-02-01", + "expiry_date": "2026-03-10", + "shelf_life_days": 37, + "quantity": 2, + "location": "冰箱", + "remark": "", + "status": "normal", + "days_left": 6, + "created_at": "2026-03-01T10:00:00Z", + "updated_at": "2026-03-01T10:00:00Z" + } + ], + "total": 15, + "page": 1, + "page_size": 20 + } +} +``` + +**状态说明**: +- `normal`: 正常(距离过期 > 7天) +- `expiring`: 即将过期(0-7天) +- `expired`: 已过期(< 0天) +- `used`: 已使用 +- `discarded`: 已丢弃 + +**days_left 计算**: +- 正数:距离过期还有 N 天 +- 0:今天过期 +- 负数:已过期 N 天 + +--- + +### 1.2 获取首页汇总 + +**接口**: `GET /api/expiry/summary` + +**响应示例**: +```json +{ + "code": 0, + "message": "success", + "data": { + "total_items": 15, + "expiring_soon": 3, + "expired": 2, + "normal": 10, + "used": 0, + "discarded": 0 + } +} +``` + +**字段说明**: +- `total_items`: 当前有效物品总数(不含已使用/已丢弃) +- `expiring_soon`: 7天内即将过期的数量 +- `expired`: 已过期的数量 +- `normal`: 正常状态的数量 +- `used`: 已使用的数量 +- `discarded`: 已丢弃的数量 + +--- + +### 1.3 添加物品 + +**接口**: `POST /api/expiry/items` + +**请求体**: +```json +{ + "name": "牛奶", + "category": "food", + "production_date": "2026-02-01", + "expiry_date": "2026-03-10", + "shelf_life_days": 37, + "quantity": 2, + "location": "冰箱", + "remark": "伊利纯牛奶" +} +``` + +**字段说明**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| name | string | ✅ | 物品名称,最长 100 字符 | +| category | string | ✅ | 分类:food/medicine/cosmetic/other | +| production_date | string | ❌ | 生产日期,格式 YYYY-MM-DD | +| expiry_date | string | ✅ | 过期日期,格式 YYYY-MM-DD | +| shelf_life_days | int | ❌ | 保质期天数 | +| quantity | int | ❌ | 数量,默认 1 | +| location | string | ❌ | 存放位置,最长 50 字符 | +| remark | string | ❌ | 备注,最长 255 字符 | + +**业务逻辑**: +1. 如果提供 `production_date` 和 `shelf_life_days`,后端会自动计算 `expiry_date`(如果未提供) +2. `expiry_date` 必填,或者 `production_date` + `shelf_life_days` 组合必填 +3. 初始状态为 `normal`,后端根据 `expiry_date` 自动计算 `days_left` + +**响应示例**: +```json +{ + "code": 0, + "message": "添加成功", + "data": { + "id": 1, + "name": "牛奶", + "category": "food", + "production_date": "2026-02-01", + "expiry_date": "2026-03-10", + "shelf_life_days": 37, + "quantity": 2, + "location": "冰箱", + "remark": "伊利纯牛奶", + "status": "normal", + "days_left": 6, + "created_at": "2026-03-04T10:00:00Z", + "updated_at": "2026-03-04T10:00:00Z" + } +} +``` + +--- + +### 1.4 更新物品 + +**接口**: `PUT /api/expiry/items/:id` + +**路径参数**: +- `id`: 物品 ID + +**请求体**: 同添加物品 + +**响应示例**: +```json +{ + "code": 0, + "message": "更新成功", + "data": { + "id": 1, + "name": "牛奶(已开封)", + ... + } +} +``` + +--- + +### 1.5 删除物品 + +**接口**: `DELETE /api/expiry/items/:id` + +**路径参数**: +- `id`: 物品 ID + +**响应示例**: +```json +{ + "code": 0, + "message": "删除成功" +} +``` + +**说明**: 使用软删除,数据不会真正删除 + +--- + +### 1.6 标记物品状态 + +**接口**: `POST /api/expiry/items/:id/status` + +**路径参数**: +- `id`: 物品 ID + +**请求体**: +```json +{ + "status": "used" +} +``` + +**status 可选值**: +- `used`: 已使用 +- `discarded`: 已丢弃 + +**响应示例**: +```json +{ + "code": 0, + "message": "标记成功", + "data": { + "id": 1, + "status": "used", + "updated_at": "2026-03-04T10:00:00Z" + } +} +``` + +--- + +## 2. 用户设置 + +### 2.1 获取用户设置 + +**接口**: `GET /api/expiry/settings` + +**响应示例**: +```json +{ + "code": 0, + "message": "success", + "data": { + "remind_days": [7, 3, 1] + } +} +``` + +**说明**: 如果用户未设置,返回默认值 `[7, 3, 1]` + +--- + +### 2.2 更新用户设置 + +**接口**: `POST /api/expiry/settings` + +**请求体**: +```json +{ + "remind_days": [10, 5, 2, 1] +} +``` + +**字段说明**: +- `remind_days`: 提醒天数数组,最多 5 个值,每个值范围 1-30 + +**响应示例**: +```json +{ + "code": 0, + "message": "更新成功", + "data": { + "remind_days": [10, 5, 2, 1] + } +} +``` + +--- + +## 3. 错误码 + +| code | message | 说明 | +|------|---------|------| +| 0 | success | 成功 | +| 400 | 参数错误 | 请求参数不合法 | +| 401 | 未授权 | Token 无效或过期 | +| 404 | 资源不存在 | 物品不存在 | +| 500 | 服务器错误 | 内部错误 | + +**错误响应示例**: +```json +{ + "code": 400, + "message": "物品名称不能为空" +} +``` + +--- + +## 4. 前端集成示例 + +### 4.1 获取首页数据 + +```javascript +// 并行请求 +Promise.all([ + wx.request({ url: '/api/expiry/summary' }), + wx.request({ url: '/api/expiry/items?status=expiring&page_size=10' }) +]).then(([summary, items]) => { + // 渲染首页 +}); +``` + +### 4.2 添加物品 + +```javascript +wx.request({ + url: '/api/expiry/items', + method: 'POST', + data: { + name: '牛奶', + category: 'food', + expiry_date: '2026-03-10', + quantity: 2, + location: '冰箱' + }, + success: (res) => { + if (res.data.code === 0) { + wx.showToast({ title: '添加成功' }); + // 刷新列表 + } + } +}); +``` + +### 4.3 标记已使用 + +```javascript +wx.request({ + url: `/api/expiry/items/${itemId}/status`, + method: 'POST', + data: { status: 'used' }, + success: (res) => { + if (res.data.code === 0) { + wx.showToast({ title: '已标记' }); + // 刷新列表 + } + } +}); +``` + +--- + +## 5. 性能建议 + +### 5.1 缓存策略 + +- **summary**: 进入首页时刷新,缓存 5 分钟 +- **items 列表**: 下拉刷新时更新,支持分页加载 +- **settings**: 登录后缓存,修改时更新 + +### 5.2 请求优化 + +- 首页使用并行请求 `summary` + `items` +- 列表滚动到底部时自动加载下一页 +- 使用防抖避免频繁请求 + +--- + +## 6. 数据库索引建议 + +```sql +-- 用户 + 过期日期(最常用查询) +CREATE INDEX idx_user_expiry ON expiry_items(user_id, expiry_date); + +-- 用户 + 分类 +CREATE INDEX idx_user_category ON expiry_items(user_id, category); + +-- 用户 + 状态 +CREATE INDEX idx_user_status ON expiry_items(user_id, status); + +-- 软删除 +CREATE INDEX idx_deleted_at ON expiry_items(deleted_at); +``` diff --git a/docs/expiry/DEVELOPMENT.md b/docs/expiry/DEVELOPMENT.md new file mode 100644 index 0000000..e5c8c86 --- /dev/null +++ b/docs/expiry/DEVELOPMENT.md @@ -0,0 +1,560 @@ +# 保质期提醒小程序 - 开发指南 + +本文档提供技术实现细节和开发规范。 + +--- + +## 1. 技术架构 + +### 1.1 整体架构 + +``` +┌─────────────────┐ +│ 微信小程序端 │ +│ (原生开发) │ +└────────┬────────┘ + │ HTTPS + │ JWT Token +┌────────▼────────┐ +│ Gin Router │ +│ (路由层) │ +└────────┬────────┘ + │ +┌────────▼────────┐ +│ Handler │ +│ (控制器层) │ +└────────┬────────┘ + │ +┌────────▼────────┐ +│ Service │ +│ (业务逻辑层) │ +└────────┬────────┘ + │ +┌────────▼────────┐ +│ Repository │ +│ (数据访问层) │ +└────────┬────────┘ + │ +┌────────▼────────┐ +│ MySQL │ +│ (数据存储) │ +└─────────────────┘ +``` + +### 1.2 目录结构 + +``` +internal/expiry/ +├── handler.go # HTTP 处理器 +├── service.go # 业务逻辑 +├── repository.go # 数据访问 +├── model.go # 数据模型 +└── dto.go # 数据传输对象(可选) + +docs/expiry/ +├── PRD.md # 产品需求文档 +├── API.md # 接口文档 +├── README.md # 项目说明 +├── ISSUES.md # 开发任务 +└── DEVELOPMENT.md # 本文档 + +docs/sql/ +└── expiry.sql # 数据库初始化脚本 +``` + +--- + +## 2. 核心实现 + +### 2.1 过期状态计算 + +**关键逻辑**: +```go +// 计算剩余天数 +func (item *ExpiryItem) CalculateDaysLeft() int { + now := time.Now().Truncate(24 * time.Hour) + expiry := item.ExpiryDate.Truncate(24 * time.Hour) + return int(expiry.Sub(now).Hours() / 24) +} + +// 计算状态 +func (item *ExpiryItem) CalculateStatus() string { + daysLeft := item.CalculateDaysLeft() + if daysLeft < 0 { + return "expired" + } else if daysLeft <= 7 { + return "expiring" + } + return "normal" +} +``` + +**注意事项**: +- 使用 `Truncate(24 * time.Hour)` 确保只比较日期,忽略时分秒 +- 状态计算在查询时动态进行,不存储到数据库 +- 前端根据 `days_left` 显示不同颜色 + +### 2.2 自动计算过期日期 + +**场景**:用户填写生产日期 + 保质期天数,自动计算过期日期 + +```go +func (s *ExpiryService) CreateItem(userID uint, req CreateItemRequest) (*ExpiryItem, error) { + item := &ExpiryItem{ + UserID: userID, + MiniProgramID: req.MiniProgramID, + Name: req.Name, + Category: req.Category, + ProductionDate: req.ProductionDate, + Quantity: req.Quantity, + Location: req.Location, + Remark: req.Remark, + } + + // 如果提供了生产日期和保质期天数,自动计算过期日期 + if req.ProductionDate != nil && req.ShelfLifeDays != nil { + expiryDate := req.ProductionDate.AddDate(0, 0, *req.ShelfLifeDays) + item.ExpiryDate = expiryDate + item.ShelfLifeDays = req.ShelfLifeDays + } else if req.ExpiryDate != nil { + item.ExpiryDate = *req.ExpiryDate + } else { + return nil, errors.New("必须提供过期日期或生产日期+保质期天数") + } + + if err := s.repo.Create(item); err != nil { + return nil, err + } + + return item, nil +} +``` + +### 2.3 汇总统计实现 + +**SQL 查询优化**: +```go +func (r *ExpiryRepository) GetSummary(userID uint) (map[string]int, error) { + var result struct { + TotalItems int64 + ExpiringSoon int64 + Expired int64 + Normal int64 + Used int64 + Discarded int64 + } + + now := time.Now().Truncate(24 * time.Hour) + sevenDaysLater := now.AddDate(0, 0, 7) + + db := r.db.Model(&ExpiryItem{}).Where("user_id = ?", userID) + + // 总数(不含已使用/已丢弃) + db.Where("status NOT IN ?", []string{"used", "discarded"}).Count(&result.TotalItems) + + // 即将过期(7天内) + db.Where("expiry_date BETWEEN ? AND ?", now, sevenDaysLater). + Where("status NOT IN ?", []string{"used", "discarded"}). + Count(&result.ExpiringSoon) + + // 已过期 + db.Where("expiry_date < ?", now). + Where("status NOT IN ?", []string{"used", "discarded"}). + Count(&result.Expired) + + // 正常 + db.Where("expiry_date > ?", sevenDaysLater). + Where("status NOT IN ?", []string{"used", "discarded"}). + Count(&result.Normal) + + // 已使用 + db.Where("status = ?", "used").Count(&result.Used) + + // 已丢弃 + db.Where("status = ?", "discarded").Count(&result.Discarded) + + return map[string]int{ + "total_items": int(result.TotalItems), + "expiring_soon": int(result.ExpiringSoon), + "expired": int(result.Expired), + "normal": int(result.Normal), + "used": int(result.Used), + "discarded": int(result.Discarded), + }, nil +} +``` + +**优化建议**: +- 使用索引 `idx_user_expiry (user_id, expiry_date)` +- 考虑使用 Redis 缓存(5分钟过期) + +### 2.4 列表查询与筛选 + +**Repository 实现**: +```go +func (r *ExpiryRepository) FindByUser( + userID uint, + filters map[string]interface{}, + page, pageSize int, +) ([]ExpiryItem, int64, error) { + var items []ExpiryItem + var total int64 + + db := r.db.Model(&ExpiryItem{}).Where("user_id = ?", userID) + + // 状态筛选 + if status, ok := filters["status"].(string); ok && status != "all" { + if status == "expiring" { + now := time.Now().Truncate(24 * time.Hour) + sevenDaysLater := now.AddDate(0, 0, 7) + db = db.Where("expiry_date BETWEEN ? AND ?", now, sevenDaysLater) + } else if status == "expired" { + now := time.Now().Truncate(24 * time.Hour) + db = db.Where("expiry_date < ?", now) + } else { + db = db.Where("status = ?", status) + } + } + + // 分类筛选 + if category, ok := filters["category"].(string); ok && category != "all" { + db = db.Where("category = ?", category) + } + + // 排序 + sort := "expiry_date" + if s, ok := filters["sort"].(string); ok { + sort = s + } + db = db.Order(sort + " ASC") + + // 统计总数 + db.Count(&total) + + // 分页 + offset := (page - 1) * pageSize + db = db.Offset(offset).Limit(pageSize) + + if err := db.Find(&items).Error; err != nil { + return nil, 0, err + } + + return items, total, nil +} +``` + +--- + +## 3. 前端实现要点 + +### 3.1 API 请求封装 + +**utils/request.js**: +```javascript +const BASE_URL = 'https://your-domain.com/api'; + +function request(options) { + const token = wx.getStorageSync('token'); + + return new Promise((resolve, reject) => { + wx.request({ + url: BASE_URL + options.url, + method: options.method || 'GET', + data: options.data || {}, + header: { + 'Content-Type': 'application/json', + 'Authorization': token ? `Bearer ${token}` : '' + }, + success: (res) => { + if (res.data.code === 0) { + resolve(res.data.data); + } else if (res.data.code === 401) { + // Token 过期,重新登录 + wx.navigateTo({ url: '/pages/login/login' }); + reject(res.data); + } else { + wx.showToast({ + title: res.data.message || '请求失败', + icon: 'none' + }); + reject(res.data); + } + }, + fail: reject + }); + }); +} + +module.exports = { request }; +``` + +### 3.2 日期格式化 + +**utils/date.js**: +```javascript +// 计算剩余天数颜色 +function getDaysLeftColor(daysLeft) { + if (daysLeft < 0) return '#FF4444'; // 红色:已过期 + if (daysLeft <= 3) return '#FF8800'; // 橙色:3天内 + if (daysLeft <= 7) return '#FFBB00'; // 黄色:7天内 + return '#00CC66'; // 绿色:正常 +} + +// 格式化日期 +function formatDate(date) { + const d = new Date(date); + const year = d.getFullYear(); + const month = String(d.getMonth() + 1).padStart(2, '0'); + const day = String(d.getDate()).padStart(2, '0'); + return `${year}-${month}-${day}`; +} + +// 计算剩余天数文本 +function getDaysLeftText(daysLeft) { + if (daysLeft < 0) return `已过期 ${Math.abs(daysLeft)} 天`; + if (daysLeft === 0) return '今天过期'; + if (daysLeft === 1) return '明天过期'; + return `还剩 ${daysLeft} 天`; +} + +module.exports = { + getDaysLeftColor, + formatDate, + getDaysLeftText +}; +``` + +### 3.3 分类图标映射 + +**utils/category.js**: +```javascript +const CATEGORY_CONFIG = { + food: { + name: '食品', + icon: '🍎', + color: '#FF6B6B' + }, + medicine: { + name: '药品', + icon: '💊', + color: '#4ECDC4' + }, + cosmetic: { + name: '化妆品', + icon: '💄', + color: '#FFB6C1' + }, + other: { + name: '其他', + icon: '📦', + color: '#95A5A6' + } +}; + +function getCategoryConfig(category) { + return CATEGORY_CONFIG[category] || CATEGORY_CONFIG.other; +} + +module.exports = { getCategoryConfig, CATEGORY_CONFIG }; +``` + +--- + +## 4. 数据库优化 + +### 4.1 索引设计 + +```sql +-- 用户 + 过期日期(最常用查询) +CREATE INDEX idx_user_expiry ON expiry_items(user_id, expiry_date); + +-- 用户 + 分类 +CREATE INDEX idx_user_category ON expiry_items(user_id, category); + +-- 用户 + 状态 +CREATE INDEX idx_user_status ON expiry_items(user_id, status); + +-- 软删除 +CREATE INDEX idx_deleted_at ON expiry_items(deleted_at); +``` + +### 4.2 查询优化建议 + +1. **避免全表扫描**:所有查询都带上 `user_id` +2. **使用覆盖索引**:常用字段加入索引 +3. **分页查询**:使用 `LIMIT` 和 `OFFSET` +4. **缓存热点数据**:summary 数据使用 Redis 缓存 + +--- + +## 5. 安全性考虑 + +### 5.1 权限控制 + +- 所有接口必须通过 JWT 认证 +- 查询时必须验证 `user_id`,防止越权访问 +- 删除操作使用软删除,可恢复 + +### 5.2 输入验证 + +```go +// 验证物品名称 +if len(req.Name) == 0 || len(req.Name) > 100 { + return nil, errors.New("物品名称长度必须在1-100之间") +} + +// 验证分类 +validCategories := []string{"food", "medicine", "cosmetic", "other"} +if !contains(validCategories, req.Category) { + return nil, errors.New("无效的分类") +} + +// 验证日期 +if req.ExpiryDate != nil && req.ExpiryDate.Before(time.Now()) { + // 允许添加已过期物品,只是警告 + // 可根据业务需求调整 +} +``` + +--- + +## 6. 测试策略 + +### 6.1 单元测试 + +**测试 Model 层**: +```go +func TestCalculateDaysLeft(t *testing.T) { + item := &ExpiryItem{ + ExpiryDate: time.Now().AddDate(0, 0, 5), + } + daysLeft := item.CalculateDaysLeft() + assert.Equal(t, 5, daysLeft) +} + +func TestCalculateStatus(t *testing.T) { + tests := []struct { + daysLeft int + expected string + }{ + {-1, "expired"}, + {0, "expiring"}, + {5, "expiring"}, + {7, "expiring"}, + {8, "normal"}, + } + + for _, tt := range tests { + item := &ExpiryItem{ + ExpiryDate: time.Now().AddDate(0, 0, tt.daysLeft), + } + assert.Equal(t, tt.expected, item.CalculateStatus()) + } +} +``` + +### 6.2 集成测试 + +使用 Postman 或编写测试脚本: +```bash +# 登录获取 Token +TOKEN=$(curl -X POST http://localhost:8080/api/auth/login \ + -d '{"code":"xxx","mini_program_id":1}' | jq -r '.data.token') + +# 添加物品 +curl -X POST http://localhost:8080/api/expiry/items \ + -H "Authorization: Bearer $TOKEN" \ + -d '{ + "name": "牛奶", + "category": "food", + "expiry_date": "2026-03-10", + "quantity": 2 + }' + +# 获取列表 +curl -X GET "http://localhost:8080/api/expiry/items?status=expiring" \ + -H "Authorization: Bearer $TOKEN" +``` + +--- + +## 7. 部署建议 + +### 7.1 环境变量 + +```bash +# .env +SERVER_PORT=8080 +DB_HOST=localhost +DB_PORT=3306 +DB_USER=root +DB_PASSWORD=your_password +DB_NAME=wx_service +JWT_SECRET=your_jwt_secret +GIN_MODE=release +``` + +### 7.2 Docker 部署(可选) + +```dockerfile +FROM golang:1.23-alpine AS builder +WORKDIR /app +COPY . . +RUN go build -o wx_service ./cmd/api + +FROM alpine:latest +WORKDIR /app +COPY --from=builder /app/wx_service . +COPY .env . +EXPOSE 8080 +CMD ["./wx_service"] +``` + +--- + +## 8. 监控与日志 + +### 8.1 日志记录 + +```go +// 记录关键操作 +log.Printf("[Expiry] User %d created item: %s", userID, item.Name) +log.Printf("[Expiry] User %d deleted item: %d", userID, itemID) +``` + +### 8.2 性能监控 + +- 接口响应时间 +- 数据库查询耗时 +- 错误率统计 + +--- + +## 9. 常见问题 + +### Q1: 时区问题如何处理? + +统一使用 UTC 时间存储,前端根据用户时区显示。Go 中使用 `time.Now().UTC()`。 + +### Q2: 如何处理大量过期物品? + +定期清理已过期且标记为 `discarded` 的物品(物理删除),或者归档到历史表。 + +### Q3: 如何优化首页加载速度? + +- 使用 Redis 缓存 summary 数据 +- 首页只加载即将过期的物品(限制数量) +- 使用 CDN 加速静态资源 + +--- + +## 10. 后续优化方向 + +1. **OCR 识别**:集成 OCR 识别生产日期和保质期 +2. **推送通知**:使用小程序订阅消息推送临期提醒 +3. **数据分析**:统计用户消费习惯,提供采购建议 +4. **社交功能**:家庭成员共享物品列表 + +--- + +**文档维护**: 随着项目迭代,及时更新本文档 diff --git a/docs/expiry/ISSUES.md b/docs/expiry/ISSUES.md new file mode 100644 index 0000000..48fe9be --- /dev/null +++ b/docs/expiry/ISSUES.md @@ -0,0 +1,463 @@ +# 保质期提醒小程序 - 开发任务清单 + +本文档按优先级和依赖关系拆分开发任务,便于团队协作和进度跟踪。 + +--- + +## 🎯 Milestone 1: 数据库与基础架构(优先级:P0) + +### Issue #1: 数据库表设计与初始化 +**标签**: `database`, `P0` +**预计工时**: 0.5天 + +**任务描述**: +- [ ] 创建 `expiry_items` 表(物品表) +- [ ] 创建 `expiry_user_settings` 表(用户设置表) +- [ ] 添加必要的索引 +- [ ] 编写 SQL 初始化脚本 `docs/sql/expiry.sql` + +**验收标准**: +- 表结构符合 PRD 设计 +- 索引覆盖常用查询场景 +- 可通过脚本一键初始化 + +**SQL 脚本位置**: `/root/wx_service/docs/sql/expiry.sql` + +--- + +### Issue #2: 创建 expiry 模块目录结构 +**标签**: `backend`, `architecture`, `P0` +**预计工时**: 0.5天 + +**任务描述**: +- [ ] 创建 `internal/expiry` 目录 +- [ ] 创建 `model.go` - 定义数据模型 +- [ ] 创建 `repository.go` - 数据访问层 +- [ ] 创建 `service.go` - 业务逻辑层 +- [ ] 创建 `handler.go` - HTTP 处理器 +- [ ] 在 `internal/routes/routes.go` 注册路由 + +**目录结构**: +``` +internal/expiry/ +├── handler.go +├── service.go +├── model.go +└── repository.go +``` + +**验收标准**: +- 目录结构清晰,符合项目规范 +- 路由注册成功,可通过 `/api/expiry/healthz` 测试 + +--- + +## 🔧 Milestone 2: 后端核心接口(优先级:P0) + +### Issue #3: 实现物品数据模型 +**标签**: `backend`, `model`, `P0` +**预计工时**: 0.5天 +**依赖**: Issue #1, #2 + +**任务描述**: +- [ ] 定义 `ExpiryItem` 结构体 +- [ ] 定义 `ExpiryUserSettings` 结构体 +- [ ] 实现 GORM 模型标签 +- [ ] 实现 `CalculateDaysLeft()` 方法 +- [ ] 实现 `CalculateStatus()` 方法 + +**核心逻辑**: +```go +type ExpiryItem struct { + ID uint `json:"id" gorm:"primaryKey"` + UserID uint `json:"user_id"` + MiniProgramID uint `json:"mini_program_id"` + Name string `json:"name"` + Category string `json:"category"` + ProductionDate *time.Time `json:"production_date"` + ExpiryDate time.Time `json:"expiry_date"` + ShelfLifeDays *int `json:"shelf_life_days"` + Quantity int `json:"quantity" gorm:"default:1"` + Location string `json:"location"` + Remark string `json:"remark"` + Status string `json:"status" gorm:"default:'normal'"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `json:"-" gorm:"index"` +} + +// 计算剩余天数 +func (item *ExpiryItem) CalculateDaysLeft() int { + now := time.Now().Truncate(24 * time.Hour) + expiry := item.ExpiryDate.Truncate(24 * time.Hour) + return int(expiry.Sub(now).Hours() / 24) +} + +// 计算状态 +func (item *ExpiryItem) CalculateStatus() string { + daysLeft := item.CalculateDaysLeft() + if daysLeft < 0 { + return "expired" + } else if daysLeft <= 7 { + return "expiring" + } + return "normal" +} +``` + +**验收标准**: +- 模型定义完整,字段类型正确 +- 辅助方法逻辑正确 +- 通过单元测试 + +--- + +### Issue #4: 实现物品 Repository 层 +**标签**: `backend`, `repository`, `P0` +**预计工时**: 1天 +**依赖**: Issue #3 + +**任务描述**: +- [ ] `Create(item *ExpiryItem) error` - 创建物品 +- [ ] `Update(item *ExpiryItem) error` - 更新物品 +- [ ] `Delete(id, userID uint) error` - 删除物品(软删除) +- [ ] `FindByID(id, userID uint) (*ExpiryItem, error)` - 查询单个物品 +- [ ] `FindByUser(userID uint, filters map[string]interface{}, page, pageSize int) ([]ExpiryItem, int64, error)` - 查询用户物品列表 +- [ ] `GetSummary(userID uint) (map[string]int, error)` - 获取统计汇总 +- [ ] `UpdateStatus(id, userID uint, status string) error` - 更新状态 + +**验收标准**: +- 所有方法实现完整 +- 支持分页、筛选、排序 +- 错误处理完善 + +--- + +### Issue #5: 实现物品 Service 层 +**标签**: `backend`, `service`, `P0` +**预计工时**: 1天 +**依赖**: Issue #4 + +**任务描述**: +- [ ] `CreateItem(userID uint, req CreateItemRequest) (*ExpiryItem, error)` - 创建物品 + - 验证必填字段 + - 如果提供生产日期+保质期,自动计算过期日期 + - 调用 Repository 创建 +- [ ] `UpdateItem(id, userID uint, req UpdateItemRequest) (*ExpiryItem, error)` - 更新物品 +- [ ] `DeleteItem(id, userID uint) error` - 删除物品 +- [ ] `GetItem(id, userID uint) (*ExpiryItem, error)` - 获取单个物品 +- [ ] `GetItems(userID uint, filters ItemFilters) (*ItemListResponse, error)` - 获取物品列表 + - 支持状态筛选(all/expiring/expired/normal/used) + - 支持分类筛选 + - 支持排序(expiry_date/created_at) + - 计算 days_left +- [ ] `GetSummary(userID uint) (*SummaryResponse, error)` - 获取汇总统计 +- [ ] `UpdateItemStatus(id, userID uint, status string) error` - 更新状态 + +**验收标准**: +- 业务逻辑正确 +- 参数验证完善 +- 返回数据包含 days_left 计算结果 + +--- + +### Issue #6: 实现物品 Handler 层 +**标签**: `backend`, `handler`, `P0` +**预计工时**: 1天 +**依赖**: Issue #5 + +**任务描述**: +- [ ] `GetSummary(c *gin.Context)` - GET /api/expiry/summary +- [ ] `GetItems(c *gin.Context)` - GET /api/expiry/items +- [ ] `CreateItem(c *gin.Context)` - POST /api/expiry/items +- [ ] `UpdateItem(c *gin.Context)` - PUT /api/expiry/items/:id +- [ ] `DeleteItem(c *gin.Context)` - DELETE /api/expiry/items/:id +- [ ] `UpdateStatus(c *gin.Context)` - POST /api/expiry/items/:id/status + +**验收标准**: +- 所有接口可通过 Postman/curl 测试 +- 返回格式符合 API 文档 +- 错误处理完善(400/401/404/500) + +--- + +### Issue #7: 实现用户设置接口 +**标签**: `backend`, `settings`, `P1` +**预计工时**: 0.5天 +**依赖**: Issue #2 + +**任务描述**: +- [ ] Repository: `GetSettings(userID uint)`, `UpdateSettings(userID uint, remindDays []int)` +- [ ] Service: 验证 remind_days 数组(最多5个,范围1-30) +- [ ] Handler: `GetSettings(c *gin.Context)`, `UpdateSettings(c *gin.Context)` + +**验收标准**: +- 默认返回 [7,3,1] +- 更新后持久化到数据库 + +--- + +## 📱 Milestone 3: 小程序前端开发(优先级:P0) + +### Issue #8: 小程序项目初始化 +**标签**: `frontend`, `setup`, `P0` +**预计工时**: 0.5天 + +**任务描述**: +- [ ] 创建小程序项目 +- [ ] 配置 `app.json`(页面路由、TabBar) +- [ ] 配置 `project.config.json` +- [ ] 封装 API 请求工具(支持 JWT Token) +- [ ] 实现登录逻辑(复用公共登录接口) + +**TabBar 配置**: +```json +{ + "list": [ + { + "pagePath": "pages/expiry/home/home", + "text": "首页", + "iconPath": "images/home.png", + "selectedIconPath": "images/home-active.png" + }, + { + "pagePath": "pages/expiry/list/list", + "text": "全部", + "iconPath": "images/list.png", + "selectedIconPath": "images/list-active.png" + }, + { + "pagePath": "pages/expiry/profile/profile", + "text": "我的", + "iconPath": "images/profile.png", + "selectedIconPath": "images/profile-active.png" + } + ] +} +``` + +**验收标准**: +- 小程序可正常运行 +- 登录流程正常 +- API 请求工具可用 + +--- + +### Issue #9: 首页开发 +**标签**: `frontend`, `home`, `P0` +**预计工时**: 1.5天 +**依赖**: Issue #6, #8 + +**任务描述**: +- [ ] 统计卡片组件(总数/即将过期/已过期) +- [ ] 即将过期物品列表 +- [ ] 列表项组件(名称、分类图标、剩余天数、过期日期) +- [ ] 浮动添加按钮 +- [ ] 下拉刷新 +- [ ] 空状态提示 + +**UI 要点**: +- 剩余天数颜色:红色(已过期)、橙色(3天内)、黄色(7天内)、绿色(正常) +- 分类图标:食品🍎、药品💊、化妆品💄、其他📦 + +**验收标准**: +- 数据正确展示 +- 交互流畅 +- 空状态友好 + +--- + +### Issue #10: 添加/编辑页开发 +**标签**: `frontend`, `form`, `P0` +**预计工时**: 1.5天 +**依赖**: Issue #6, #8 + +**任务描述**: +- [ ] 表单组件(名称、分类、日期、数量、位置、备注) +- [ ] 日期选择器(生产日期、过期日期) +- [ ] 分类选择器(食品/药品/化妆品/其他) +- [ ] 自动计算过期日期(生产日期 + 保质期天数) +- [ ] 表单验证 +- [ ] 提交逻辑(新增/编辑) + +**交互逻辑**: +- 如果填写生产日期 + 保质期天数,自动计算过期日期 +- 过期日期必填 + +**验收标准**: +- 表单验证完善 +- 新增/编辑功能正常 +- 自动计算逻辑正确 + +--- + +### Issue #11: 全部物品页开发 +**标签**: `frontend`, `list`, `P0` +**预计工时**: 1.5天 +**依赖**: Issue #6, #8 + +**任务描述**: +- [ ] 状态筛选 Tabs(全部/即将过期/已过期/正常) +- [ ] 分类筛选(全部/食品/药品/化妆品/其他) +- [ ] 排序切换(按过期时间/按添加时间) +- [ ] 物品列表(复用首页列表项组件) +- [ ] 左滑操作(编辑/删除/标记已用) +- [ ] 分页加载 + +**验收标准**: +- 筛选和排序功能正常 +- 左滑操作流畅 +- 分页加载正常 + +--- + +### Issue #12: 个人中心页开发 +**标签**: `frontend`, `profile`, `P1` +**预计工时**: 1天 +**依赖**: Issue #7, #8 + +**任务描述**: +- [ ] 用户信息展示(头像、昵称) +- [ ] 数据统计(累计添加、已使用、已过期) +- [ ] 提醒设置(提前天数) +- [ ] 关于小程序 + +**验收标准**: +- 用户信息正确展示 +- 设置可保存 + +--- + +## 🧪 Milestone 4: 测试与优化(优先级:P1) + +### Issue #13: 后端单元测试 +**标签**: `backend`, `test`, `P1` +**预计工时**: 1天 +**依赖**: Issue #3-#7 + +**任务描述**: +- [ ] Model 层测试(CalculateDaysLeft, CalculateStatus) +- [ ] Repository 层测试(使用 sqlmock) +- [ ] Service 层测试(业务逻辑验证) +- [ ] Handler 层测试(HTTP 请求测试) + +**验收标准**: +- 测试覆盖率 > 80% +- 所有测试通过 + +--- + +### Issue #14: 接口集成测试 +**标签**: `backend`, `test`, `P1` +**预计工时**: 0.5天 +**依赖**: Issue #6, #7 + +**任务描述**: +- [ ] 编写 Postman Collection +- [ ] 测试所有接口的正常流程 +- [ ] 测试异常情况(参数错误、权限错误等) +- [ ] 性能测试(并发请求) + +**验收标准**: +- 所有接口测试通过 +- 响应时间 < 200ms + +--- + +### Issue #15: 小程序端测试 +**标签**: `frontend`, `test`, `P1` +**预计工时**: 1天 +**依赖**: Issue #9-#12 + +**任务描述**: +- [ ] 功能测试(所有页面和交互) +- [ ] 兼容性测试(iOS/Android) +- [ ] 边界情况测试(网络异常、数据为空等) +- [ ] 性能测试(页面加载速度) + +**验收标准**: +- 所有功能正常 +- 无明显性能问题 + +--- + +### Issue #16: 性能优化 +**标签**: `optimization`, `P2` +**预计工时**: 0.5天 +**依赖**: Issue #14, #15 + +**任务描述**: +- [ ] 后端:添加 Redis 缓存(summary 数据) +- [ ] 后端:优化数据库查询(使用索引) +- [ ] 前端:列表虚拟滚动(大数据量优化) +- [ ] 前端:图片懒加载 + +**验收标准**: +- 首页加载时间 < 500ms +- 列表滚动流畅 + +--- + +## 📦 Milestone 5: 部署上线(优先级:P0) + +### Issue #17: 部署准备 +**标签**: `deployment`, `P0` +**预计工时**: 0.5天 +**依赖**: Issue #13-#15 + +**任务描述**: +- [ ] 更新 `.env.example` +- [ ] 编写部署文档 +- [ ] 配置 Docker Compose(如需要) +- [ ] 配置 Nginx(如需要) + +**验收标准**: +- 部署文档完整 +- 可一键部署 + +--- + +### Issue #18: 生产环境部署 +**标签**: `deployment`, `P0` +**预计工时**: 0.5天 +**依赖**: Issue #17 + +**任务描述**: +- [ ] 数据库初始化 +- [ ] 后端服务部署 +- [ ] 小程序提审上线 +- [ ] 监控配置 + +**验收标准**: +- 服务稳定运行 +- 小程序审核通过 + +--- + +## 📊 优先级说明 + +- **P0**: 必须完成(MVP 核心功能) +- **P1**: 重要(优化和测试) +- **P2**: 可选(性能优化) + +--- + +## 🗓️ 预计时间线 + +| Milestone | 预计工时 | 建议时间 | +|-----------|---------|---------| +| M1: 数据库与基础架构 | 1天 | Day 1 | +| M2: 后端核心接口 | 4.5天 | Day 2-5 | +| M3: 小程序前端开发 | 5天 | Day 6-10 | +| M4: 测试与优化 | 2.5天 | Day 11-12 | +| M5: 部署上线 | 1天 | Day 13 | +| **总计** | **14天** | **2周** | + +--- + +## 📝 备注 + +1. 以上工时为单人开发预估,团队协作可并行开发 +2. 建议先完成 M1+M2,再开始 M3,避免接口变更 +3. 测试阶段(M4)可与开发阶段(M2+M3)并行进行 +4. 每个 Issue 完成后需要 Code Review diff --git a/docs/expiry/PRD.md b/docs/expiry/PRD.md new file mode 100644 index 0000000..fb9bb75 --- /dev/null +++ b/docs/expiry/PRD.md @@ -0,0 +1,393 @@ +# 保质期提醒小程序 - 产品需求文档 (PRD) + +## 1. 产品概述 + +### 1.1 产品定位 +一款家庭物品保质期管理小程序,帮助用户记录食品、药品、化妆品等物品的保质期,提供临期提醒,减少浪费。 + +### 1.2 核心价值 +- **快速记录**:扫码/手动添加物品保质期 +- **智能提醒**:临期自动提醒,避免过期浪费 +- **分类管理**:按类别查看,一目了然 +- **统计分析**:了解消费习惯,优化采购 + +### 1.3 目标用户 +- 家庭主妇/主夫 +- 注重健康的年轻人 +- 有囤货习惯的用户 + +--- + +## 2. MVP 功能范围 + +### 2.1 核心功能(必做) + +#### ✅ 物品管理 +- 添加物品(名称、分类、生产日期、保质期天数) +- 编辑物品 +- 删除物品 +- 标记已使用/已丢弃 + +#### ✅ 提醒系统 +- 临期提醒(提前 7 天、3 天、1 天) +- 已过期标记 +- 首页展示即将过期物品列表 + +#### ✅ 分类查看 +- 预设分类:食品、药品、化妆品、其他 +- 按分类筛选 +- 按过期时间排序 + +#### ✅ 基础统计 +- 当前物品总数 +- 即将过期数量(7天内) +- 已过期数量 + +### 2.2 暂不实现(后续迭代) + +- ❌ 拍照识别生产日期 +- ❌ 家庭成员共享 +- ❌ 购物清单联动 +- ❌ 消费习惯分析 +- ❌ 推送通知(小程序订阅消息) + +--- + +## 3. 页面结构 + +### 3.1 首页 (home) + +**核心目标**:快速查看即将过期物品 + +| 元素 | 说明 | 数据来源 | +|------|------|----------| +| 统计卡片 | 总数/即将过期/已过期 | `GET /expiry/summary` | +| 即将过期列表 | 7天内过期的物品 | `GET /expiry/items?status=expiring` | +| 快速添加按钮 | 浮动按钮 | 跳转添加页 | +| 分类筛选 | 全部/食品/药品/化妆品/其他 | 前端筛选 | + +**列表项展示**: +- 物品名称 +- 分类图标 +- 剩余天数(红色:已过期,橙色:3天内,黄色:7天内) +- 过期日期 + +### 3.2 添加/编辑页 (add_edit_item) + +**表单字段**: + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| name | string | ✅ | 物品名称 | +| category | enum | ✅ | 分类:food/medicine/cosmetic/other | +| production_date | date | ❌ | 生产日期 | +| expiry_date | date | ✅ | 过期日期 | +| shelf_life_days | int | ❌ | 保质期天数(与过期日期二选一) | +| quantity | int | ❌ | 数量(默认1) | +| location | string | ❌ | 存放位置(冰箱/柜子等) | +| remark | string | ❌ | 备注 | + +**交互逻辑**: +- 如果填写生产日期 + 保质期天数,自动计算过期日期 +- 如果直接填写过期日期,则不需要生产日期 + +### 3.3 全部物品页 (all_items) + +**核心目标**:查看所有物品,支持筛选和排序 + +| 功能 | 说明 | +|------|------| +| 状态筛选 | 全部/即将过期/已过期/正常 | +| 分类筛选 | 全部/食品/药品/化妆品/其他 | +| 排序方式 | 按过期时间/按添加时间 | +| 列表展示 | 同首页列表项 | +| 左滑操作 | 编辑/删除/标记已用 | + +### 3.4 个人中心 (profile) + +**功能**: +- 用户信息(头像、昵称) +- 提醒设置(提前天数:7/3/1天) +- 数据统计(累计添加、已使用、已过期) +- 关于小程序 + +--- + +## 4. 数据模型 + +### 4.1 物品表 (expiry_items) + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | 主键 | +| user_id | bigint | 用户ID(关联 users 表) | +| mini_program_id | bigint | 小程序ID | +| name | varchar(100) | 物品名称 | +| category | varchar(20) | 分类:food/medicine/cosmetic/other | +| production_date | date | 生产日期(可选) | +| expiry_date | date | 过期日期 | +| shelf_life_days | int | 保质期天数(可选) | +| quantity | int | 数量(默认1) | +| location | varchar(50) | 存放位置(可选) | +| remark | varchar(255) | 备注(可选) | +| status | varchar(20) | 状态:normal/used/expired/discarded | +| created_at | timestamp | 创建时间 | +| updated_at | timestamp | 更新时间 | +| deleted_at | timestamp | 软删除 | + +**索引**: +- `idx_user_expiry` (user_id, expiry_date) +- `idx_user_category` (user_id, category) +- `idx_user_status` (user_id, status) + +### 4.2 用户设置表 (expiry_user_settings) + +| 字段 | 类型 | 说明 | +|------|------|------| +| id | bigint | 主键 | +| user_id | bigint | 用户ID(唯一) | +| remind_days | json | 提醒天数数组 [7,3,1] | +| created_at | timestamp | 创建时间 | +| updated_at | timestamp | 更新时间 | + +--- + +## 5. API 设计 + +### 5.1 物品管理 + +#### 获取物品列表 +``` +GET /api/expiry/items +Query: + - status: string (expiring/expired/normal/used/all) + - category: string (food/medicine/cosmetic/other/all) + - sort: string (expiry_date/created_at) + - page: int + - page_size: int + +Response: +{ + "code": 0, + "data": { + "items": [ + { + "id": 1, + "name": "牛奶", + "category": "food", + "expiry_date": "2026-03-10", + "days_left": 6, + "status": "normal", + "quantity": 2, + "location": "冰箱" + } + ], + "total": 10, + "page": 1, + "page_size": 20 + } +} +``` + +#### 获取首页汇总 +``` +GET /api/expiry/summary + +Response: +{ + "code": 0, + "data": { + "total_items": 15, + "expiring_soon": 3, // 7天内 + "expired": 2, + "normal": 10 + } +} +``` + +#### 添加物品 +``` +POST /api/expiry/items +Body: +{ + "name": "牛奶", + "category": "food", + "production_date": "2026-02-01", // 可选 + "expiry_date": "2026-03-10", // 必填 + "shelf_life_days": 37, // 可选 + "quantity": 2, + "location": "冰箱", + "remark": "" +} + +Response: +{ + "code": 0, + "data": { + "id": 1, + "name": "牛奶", + ... + } +} +``` + +#### 更新物品 +``` +PUT /api/expiry/items/:id +Body: 同添加物品 + +Response: +{ + "code": 0, + "data": { ... } +} +``` + +#### 删除物品 +``` +DELETE /api/expiry/items/:id + +Response: +{ + "code": 0, + "message": "删除成功" +} +``` + +#### 标记物品状态 +``` +POST /api/expiry/items/:id/status +Body: +{ + "status": "used" // used/discarded +} + +Response: +{ + "code": 0, + "message": "标记成功" +} +``` + +### 5.2 用户设置 + +#### 获取设置 +``` +GET /api/expiry/settings + +Response: +{ + "code": 0, + "data": { + "remind_days": [7, 3, 1] + } +} +``` + +#### 更新设置 +``` +POST /api/expiry/settings +Body: +{ + "remind_days": [7, 3, 1] +} + +Response: +{ + "code": 0, + "message": "更新成功" +} +``` + +--- + +## 6. 用户流程 + +### 6.1 新用户流程 + +``` +启动小程序 + ↓ +微信登录 (wx.login) + ↓ +进入首页(空状态) + ↓ +点击"添加物品" + ↓ +填写表单 + ↓ +提交成功 → 返回首页 +``` + +### 6.2 日常使用流程 + +``` +打开首页 + ↓ +查看即将过期物品 + ↓ +[需要添加新物品] + ↓ +点击浮动按钮 + ↓ +填写表单 → 提交 + ↓ +[物品已使用] + ↓ +左滑 → 标记已用 + ↓ +[查看所有物品] + ↓ +切换到"全部"Tab +``` + +--- + +## 7. 技术实现要点 + +### 7.1 过期状态计算 + +**后端统一计算**: +- `days_left = expiry_date - today` +- `status`: + - `days_left < 0`: expired + - `0 <= days_left <= 7`: expiring + - `days_left > 7`: normal + +### 7.2 提醒逻辑(MVP 暂不实现推送) + +**前端展示提醒**: +- 首页红点:有即将过期物品时显示 +- 列表颜色标记: + - 红色:已过期 + - 橙色:3天内 + - 黄色:7天内 + - 绿色:正常 + +### 7.3 性能优化 + +**缓存策略**: +- summary 数据:进入首页时刷新 +- items 列表:按页加载,支持下拉刷新 + +**索引优化**: +- 按 user_id + expiry_date 查询(最常用) +- 按 user_id + category 筛选 + +--- + +## 8. 待扩展功能(后续版本) + +### V2.0 +- [ ] 拍照识别生产日期和保质期 +- [ ] 小程序订阅消息推送 +- [ ] 常用物品模板(快速添加) + +### V3.0 +- [ ] 家庭成员共享 +- [ ] 购物清单联动 +- [ ] 消费习惯分析 + +### V4.0 +- [ ] 社区分享(保质期知识) +- [ ] 积分系统(减少浪费奖励) diff --git a/docs/expiry/README.md b/docs/expiry/README.md new file mode 100644 index 0000000..0a47277 --- /dev/null +++ b/docs/expiry/README.md @@ -0,0 +1,227 @@ +# 保质期提醒小程序 - 开发文档 + +## 项目概述 + +保质期提醒小程序是一款帮助用户管理家庭物品保质期的工具,支持食品、药品、化妆品等多种物品的记录和提醒。 + +--- + +## 文档索引 + +- **[PRD.md](./PRD.md)** - 产品需求文档(功能范围、页面设计、数据模型) +- **[API.md](./API.md)** - 接口文档(完整的 API 定义和示例) +- **[DEVELOPMENT.md](./DEVELOPMENT.md)** - 开发指南(技术实现细节) +- **[ISSUES.md](./ISSUES.md)** - 开发任务清单(按优先级拆分的 Issues) + +--- + +## 快速开始 + +### 1. 数据库初始化 + +```sql +-- 创建物品表 +CREATE TABLE expiry_items ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL, + mini_program_id BIGINT UNSIGNED NOT NULL, + name VARCHAR(100) NOT NULL, + category VARCHAR(20) NOT NULL, + production_date DATE, + expiry_date DATE NOT NULL, + shelf_life_days INT, + quantity INT DEFAULT 1, + location VARCHAR(50), + remark VARCHAR(255), + status VARCHAR(20) DEFAULT 'normal', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + deleted_at TIMESTAMP NULL, + INDEX idx_user_expiry (user_id, expiry_date), + INDEX idx_user_category (user_id, category), + INDEX idx_user_status (user_id, status), + INDEX idx_deleted_at (deleted_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; + +-- 创建用户设置表 +CREATE TABLE expiry_user_settings ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT UNSIGNED NOT NULL UNIQUE, + remind_days JSON DEFAULT '[7,3,1]', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +### 2. 后端开发 + +参考现有模块结构(如 `internal/smoke`),创建 `internal/expiry` 目录: + +``` +internal/expiry/ +├── handler.go # HTTP 处理器 +├── service.go # 业务逻辑 +├── model.go # 数据模型 +└── repository.go # 数据访问层 +``` + +### 3. 路由注册 + +在 `internal/routes/routes.go` 中注册路由: + +```go +expiry := r.Group("/api/expiry") +expiry.Use(middleware.AuthMiddleware()) +{ + expiry.GET("/summary", expiryHandler.GetSummary) + expiry.GET("/items", expiryHandler.GetItems) + expiry.POST("/items", expiryHandler.CreateItem) + expiry.PUT("/items/:id", expiryHandler.UpdateItem) + expiry.DELETE("/items/:id", expiryHandler.DeleteItem) + expiry.POST("/items/:id/status", expiryHandler.UpdateStatus) + expiry.GET("/settings", expiryHandler.GetSettings) + expiry.POST("/settings", expiryHandler.UpdateSettings) +} +``` + +### 4. 小程序端开发 + +参考 API 文档进行前端开发,主要页面: + +- `pages/expiry/home` - 首页 +- `pages/expiry/add` - 添加/编辑物品 +- `pages/expiry/list` - 全部物品列表 +- `pages/expiry/profile` - 个人中心 + +--- + +## MVP 功能清单 + +### Phase 1: 核心功能(2-3天) +- [x] 数据库表设计 +- [ ] 后端 API 实现 + - [ ] 物品 CRUD + - [ ] 汇总统计 + - [ ] 用户设置 +- [ ] 前端页面开发 + - [ ] 首页(列表 + 统计) + - [ ] 添加/编辑页 + - [ ] 全部物品页 + +### Phase 2: 优化完善(1-2天) +- [ ] 状态计算逻辑优化 +- [ ] 列表分页加载 +- [ ] 左滑操作 +- [ ] 空状态提示 +- [ ] 错误处理 + +### Phase 3: 测试上线(1天) +- [ ] 单元测试 +- [ ] 接口测试 +- [ ] 小程序端测试 +- [ ] 部署上线 + +--- + +## 技术栈 + +**后端**: +- Go 1.23+ +- Gin Web Framework +- GORM (MySQL) +- JWT 认证 + +**前端**: +- 微信小程序原生开发 +- WeUI 组件库(可选) + +**数据库**: +- MySQL 8.0+ + +--- + +## 开发规范 + +### 1. 代码风格 + +遵循项目现有代码风格: +- Go: 使用 `gofmt` 格式化 +- 变量命名:驼峰命名法 +- 错误处理:统一使用 `common.ErrorResponse` + +### 2. 提交规范 + +``` +feat: 添加物品管理接口 +fix: 修复过期日期计算错误 +docs: 更新 API 文档 +test: 添加物品服务单元测试 +``` + +### 3. 分支管理 + +- `main`: 生产环境 +- `develop`: 开发环境 +- `feature/expiry-*`: 功能分支 + +--- + +## 测试数据 + +### 测试物品 + +```json +[ + { + "name": "伊利纯牛奶", + "category": "food", + "expiry_date": "2026-03-10", + "quantity": 6, + "location": "冰箱" + }, + { + "name": "感冒灵颗粒", + "category": "medicine", + "expiry_date": "2027-12-31", + "quantity": 1, + "location": "药箱" + }, + { + "name": "雅诗兰黛面霜", + "category": "cosmetic", + "production_date": "2025-06-01", + "shelf_life_days": 1095, + "quantity": 1, + "location": "梳妆台" + } +] +``` + +--- + +## 常见问题 + +### Q1: 如何计算过期状态? + +后端统一计算 `days_left = expiry_date - today`,前端根据 `days_left` 显示颜色: +- `< 0`: 红色(已过期) +- `0-3`: 橙色(即将过期) +- `4-7`: 黄色(临期) +- `> 7`: 绿色(正常) + +### Q2: 如何处理时区问题? + +统一使用 UTC 时间存储,前端根据用户时区显示。 + +### Q3: 软删除如何实现? + +使用 GORM 的 `deleted_at` 字段,查询时自动过滤已删除数据。 + +--- + +## 联系方式 + +如有问题,请查看: +- [Issues](./ISSUES.md) - 开发任务和问题追踪 +- [API 文档](./API.md) - 接口详细说明 +- [PRD](./PRD.md) - 产品需求细节 diff --git a/docs/sql/expiry.sql b/docs/sql/expiry.sql new file mode 100644 index 0000000..ba6db88 --- /dev/null +++ b/docs/sql/expiry.sql @@ -0,0 +1,129 @@ +-- 保质期提醒小程序 - 数据库初始化脚本 +-- 执行前请确保已创建数据库并选择正确的数据库 + +-- ============================================ +-- 1. 物品表 (expiry_items) +-- ============================================ +CREATE TABLE IF NOT EXISTS expiry_items ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + user_id BIGINT UNSIGNED NOT NULL COMMENT '用户ID(关联 users 表)', + mini_program_id BIGINT UNSIGNED NOT NULL COMMENT '小程序ID(关联 mini_programs 表)', + name VARCHAR(100) NOT NULL COMMENT '物品名称', + category VARCHAR(20) NOT NULL COMMENT '分类:food/medicine/cosmetic/other', + production_date DATE DEFAULT NULL COMMENT '生产日期(可选)', + expiry_date DATE NOT NULL COMMENT '过期日期', + shelf_life_days INT DEFAULT NULL COMMENT '保质期天数(可选)', + quantity INT DEFAULT 1 COMMENT '数量', + location VARCHAR(50) DEFAULT NULL COMMENT '存放位置(可选)', + remark VARCHAR(255) DEFAULT NULL COMMENT '备注(可选)', + status VARCHAR(20) DEFAULT 'normal' COMMENT '状态:normal/used/expired/discarded', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + deleted_at TIMESTAMP NULL DEFAULT NULL COMMENT '软删除时间', + + -- 索引 + INDEX idx_user_expiry (user_id, expiry_date) COMMENT '用户+过期日期索引(最常用查询)', + INDEX idx_user_category (user_id, category) COMMENT '用户+分类索引', + INDEX idx_user_status (user_id, status) COMMENT '用户+状态索引', + INDEX idx_deleted_at (deleted_at) COMMENT '软删除索引' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='保质期物品表'; + +-- ============================================ +-- 2. 用户设置表 (expiry_user_settings) +-- ============================================ +CREATE TABLE IF NOT EXISTS expiry_user_settings ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键ID', + user_id BIGINT UNSIGNED NOT NULL UNIQUE COMMENT '用户ID(唯一)', + remind_days JSON DEFAULT '[7,3,1]' COMMENT '提醒天数数组,例如 [7,3,1]', + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + + -- 索引 + INDEX idx_user_id (user_id) COMMENT '用户ID索引' +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='保质期用户设置表'; + +-- ============================================ +-- 3. 插入测试数据(可选) +-- ============================================ +-- 注意:以下测试数据需要根据实际的 user_id 和 mini_program_id 调整 + +-- 示例:假设 user_id=1, mini_program_id=1 +-- INSERT INTO expiry_items (user_id, mini_program_id, name, category, production_date, expiry_date, shelf_life_days, quantity, location, remark) VALUES +-- (1, 1, '伊利纯牛奶', 'food', '2026-02-01', '2026-03-10', 37, 6, '冰箱', ''), +-- (1, 1, '感冒灵颗粒', 'medicine', NULL, '2027-12-31', NULL, 1, '药箱', ''), +-- (1, 1, '雅诗兰黛面霜', 'cosmetic', '2025-06-01', '2028-06-01', 1095, 1, '梳妆台', ''); + +-- 插入默认用户设置 +-- INSERT INTO expiry_user_settings (user_id, remind_days) VALUES +-- (1, '[7,3,1]'); + +-- ============================================ +-- 4. 验证表结构 +-- ============================================ +-- 查看表结构 +-- SHOW CREATE TABLE expiry_items; +-- SHOW CREATE TABLE expiry_user_settings; + +-- 查看索引 +-- SHOW INDEX FROM expiry_items; +-- SHOW INDEX FROM expiry_user_settings; + +-- ============================================ +-- 5. 常用查询示例 +-- ============================================ + +-- 查询用户的所有物品 +-- SELECT * FROM expiry_items WHERE user_id = 1 AND deleted_at IS NULL; + +-- 查询即将过期的物品(7天内) +-- SELECT * FROM expiry_items +-- WHERE user_id = 1 +-- AND deleted_at IS NULL +-- AND expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 7 DAY) +-- ORDER BY expiry_date ASC; + +-- 查询已过期的物品 +-- SELECT * FROM expiry_items +-- WHERE user_id = 1 +-- AND deleted_at IS NULL +-- AND expiry_date < CURDATE() +-- ORDER BY expiry_date DESC; + +-- 统计汇总 +-- SELECT +-- COUNT(*) as total_items, +-- SUM(CASE WHEN expiry_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 7 DAY) THEN 1 ELSE 0 END) as expiring_soon, +-- SUM(CASE WHEN expiry_date < CURDATE() THEN 1 ELSE 0 END) as expired, +-- SUM(CASE WHEN expiry_date > DATE_ADD(CURDATE(), INTERVAL 7 DAY) THEN 1 ELSE 0 END) as normal +-- FROM expiry_items +-- WHERE user_id = 1 +-- AND deleted_at IS NULL +-- AND status NOT IN ('used', 'discarded'); + +-- ============================================ +-- 6. 数据清理(谨慎使用) +-- ============================================ + +-- 物理删除已软删除超过30天的记录 +-- DELETE FROM expiry_items +-- WHERE deleted_at IS NOT NULL +-- AND deleted_at < DATE_SUB(NOW(), INTERVAL 30 DAY); + +-- 清空测试数据(谨慎使用!) +-- TRUNCATE TABLE expiry_items; +-- TRUNCATE TABLE expiry_user_settings; + +-- ============================================ +-- 7. 性能优化建议 +-- ============================================ + +-- 定期分析表,优化查询性能 +-- ANALYZE TABLE expiry_items; +-- ANALYZE TABLE expiry_user_settings; + +-- 查看表状态 +-- SHOW TABLE STATUS LIKE 'expiry_%'; + +-- ============================================ +-- 初始化完成 +-- ============================================