
本文介绍如何使用 rand.perm() 生成一周内不重复的食谱序列,并通过文件持久化记录历史选择,确保同一菜品至少间隔 7 天不重复出现。
本文介绍如何使用 rand.perm() 生成一周内不重复的食谱序列,并通过文件持久化记录历史选择,确保同一菜品至少间隔 7 天不重复出现。
在日常饮食规划类 Go 应用中,简单调用 rand.Intn() 很容易导致连续多日重复推荐同一道菜(如 ID=2 的 Steak Tacos 连续两天被选中),这违背了“每周轮换”的核心需求。真正可靠的做法不是“每次随机再校验”,而是预先生成一个无重复的随机排列(permutation),并按需按日索引——这正是 Go 标准库 rand.Perm(n) 的设计初衷。
✅ 推荐方案:基于 rand.Perm() 的周序列 + 文件持久化
rand.Perm(6) 会返回一个长度为 6 的切片,包含 0~5 的随机排列(例如 [3,0,5,1,4,2])。若你有 6 道菜,可将该序列视为本周一至周六的推荐顺序(周日可复用周一或单独处理)。关键优势在于:
- 天然去重:排列本身不含重复元素;
- 确定性可控:配合固定 seed 可复现(调试友好),或每次用 time.Now().UnixNano() 保证新鲜度;
- 低开销:仅需一次计算,后续每日直接查表。
以下是一个完整可运行的增强版示例,整合了你的 Recipe 结构、序列生成与简易文件持久化(使用 JSON 存储最近一次生成的序列及生成时间):
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"math/rand"
"os"
"time"
)
type Recipe struct {
Name string `json:"name"`
PrepTime int `json:"prep_time"`
CookTime int `json:"cook_time"`
Ingredients []string `json:"ingredients"`
ID int `json:"id"`
Yield int `json:"yield"`
}
// 模拟你的食谱数据(建议后续移至配置文件或数据库)
func loadRecipes() []Recipe {
return []Recipe{
{Name: "BBQ Pulled Chicken", ID: 1, PrepTime: 25, CookTime: 5, Yield: 8},
{Name: "Steak Tacos with Pineapple", ID: 2, PrepTime: 45, CookTime: 45, Yield: 4},
{Name: "Simple Lemon Herb Chicken", ID: 3, PrepTime: 10, CookTime: 15, Yield: 2},
{Name: "Vegetable Stir Fry", ID: 4, PrepTime: 15, CookTime: 10, Yield: 4},
{Name: "Pasta Carbonara", ID: 5, PrepTime: 20, CookTime: 25, Yield: 4},
{Name: "Baked Salmon", ID: 6, PrepTime: 12, CookTime: 18, Yield: 2},
}
}
// WeeklyPlan 表示一周的食谱计划(7天),含生成时间戳用于过期判断
type WeeklyPlan struct {
Sequence []int `json:"sequence"` // 食谱ID排列,如 [3,1,6,2,5,4,1](注意:此处允许ID重复?不,应为0~len-1索引!见下方说明)
GeneratedAt int64 `json:"generated_at"`
}
const planFile = "weekly_plan.json"
// generateNewPlan 生成新的7天不重复食谱序列(基于当前所有食谱)
func generateNewPlan(recipes []Recipe) WeeklyPlan {
r := rand.New(rand.NewSource(time.Now().UnixNano()))
n := len(recipes)
if n == 0 {
panic("no recipes available")
}
// 使用 Perm 生成 0 ~ n-1 的随机排列,长度为 min(7, n)
// 若食谱数 ≥7,取前7个;若7天)则生成新计划并保存
func loadOrCreatePlan(recipes []Recipe) WeeklyPlan {
data, err := ioutil.ReadFile(planFile)
if err != nil || len(data) == 0 {
fmt.Println("No existing plan found. Generating new one...")
plan := generateNewPlan(recipes)
savePlan(plan)
return plan
}
var plan WeeklyPlan
if err := json.Unmarshal(data, &plan); err != nil {
fmt.Printf("Failed to parse plan file: %v. Regenerating...\n", err)
plan = generateNewPlan(recipes)
savePlan(plan)
return plan
}
// 检查是否过期(超过7天未更新)
if time.Since(time.Unix(plan.GeneratedAt, 0)) > 7*24*time.Hour {
fmt.Println("Plan expired. Generating new one...")
plan = generateNewPlan(recipes)
savePlan(plan)
}
return plan
}
func savePlan(plan WeeklyPlan) {
data, _ := json.MarshalIndent(plan, "", " ")
ioutil.WriteFile(planFile, data, 0644)
}
func main() {
recipes := loadRecipes()
plan := loadOrCreatePlan(recipes)
todayIdx := getTodayIndex()
if todayIdx >= len(plan.Sequence) {
todayIdx = todayIdx % len(plan.Sequence) // 安全回退
}
selectedID := plan.Sequence[todayIdx]
if selectedID <h3>⚠️ 关键注意事项</h3>
- rand.Perm(n) 返回的是索引排列,不是 ID 值本身:它生成 0,1,...,n-1 的乱序。因此你的食谱应存为切片 []Recipe,通过 recipes[perm[i]] 获取对应菜品,而非直接匹配 ID==2。
- 文件持久化需考虑并发与原子性:上述示例使用 ioutil.WriteFile,适合单用户 CLI 工具;生产环境应加锁或改用数据库。
- “至少一周不重复”的本质是周期性轮换:Perm 保证单周内不重复,而通过 GeneratedAt 时间戳控制计划有效期(>7天即刷新),自然满足跨周约束。
- 扩展建议:若需长期记忆(如“ID=2 上次出现在 3 天前,本次禁止”),则需维护一个 map[int]time.Time 的历史记录,结合 time.Since() 动态过滤候选集——但这会增加复杂度,多数场景 Perm+周期刷新 更简洁高效。
通过此方案,你不仅解决了“不重复”问题,还让程序具备了状态记忆能力,真正迈向实用化饮食规划工具。











