docs: add expiry docs and sql script

This commit is contained in:
hello-dd-code
2026-03-04 16:31:30 +08:00
parent ff16ea09d2
commit fe9f64f51d
6 changed files with 2168 additions and 0 deletions
+396
View File
@@ -0,0 +1,396 @@
# 保质期提醒小程序 - API 文档
## 基础信息
**Base URL**: `/api/expiry`
**认证方式**: JWT Token(通过公共登录接口获取,参见 `docs/common/auth.md`
**请求头**:
```
Authorization: Bearer <token>
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);
```
+560
View File
@@ -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. **社交功能**:家庭成员共享物品列表
---
**文档维护**: 随着项目迭代,及时更新本文档
+463
View File
@@ -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
+393
View File
@@ -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
- [ ] 社区分享(保质期知识)
- [ ] 积分系统(减少浪费奖励)
+227
View File
@@ -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) - 产品需求细节
+129
View File
@@ -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_%';
-- ============================================
-- 初始化完成
-- ============================================