8b1b4cde02
## 三个问题,一起解决 你问「模版要不要做成可选」,摸下来发现底下有三个更硬的: **① 根本没有多套模版可选。** 模板表唯一索引是 (species, stage) —— 同一个「猫 × 幼年期」只能有一套。「让用户选模版」在数据模型上做不到。 去掉唯一索引,加 Name / Summary / Sort / Enabled。 **② 今日任务和计划节点是两套,而且是我上一轮改出来的。** DailyTask 模板 kind=task 生成,ensureDayTasks 每天把昨天那份复制到今天 PlanTask 模板 kind=plan 生成,Day 偏移 计划改成月度管理 + 加了「应用到每天」之后,两者做的是同一件事, 但首页读 DailyTask、计划页管 PlanTask —— 用户在计划页加一条「每天喂药」, 首页今日任务里看不到。 DailyTask 退役。task.go 重写成 PlanTask 的一层「按日期取」的门面, /pets/:id/tasks 路径不变,小程序端不用为这次合并改调用。 PlanTask 加 Priority(首页那份要能标出哪条重要)。 顺手删掉重复的 CreateTask/UpdateTask/DeleteTask —— /plan-tasks 那套已经 覆盖了,留着两套会出现「首页管理弹层加的任务」和「计划页加的节点」进不同的表。 **③ 模板的 Day 偏移和月度计划对不上。** 月底建档的话 day=14/21/30 全落到 下个月,用户建完档打开计划页发现本月是空的。 改成重复规则:once(偏移 N 天,疫苗序列要用)/ daily / weekly(星期几) / monthly(几号)。套用 = 展开成目标月份的具体日期。 「每月 31 号」在 2 月按月末算,不然那个月直接没有。 列名用 repeat_rule 不用 repeat —— repeat 是 MySQL 保留字(和 group_key 同理)。 ## 展开成实体行,不是「存规则读时展开」 勾选状态是按天的。存规则得再开一张表记「哪天打过勾」,而且用户把 「每周二梳毛」这周挪到周三就要引入例外记录。一次展开成行,增删改和打勾全免费。 代价是套用后模板和计划断开:后台改模板不影响已套用的宠物。这是有意的—— 用户已经在自己的计划上勾了几十个勾,后台一改就被覆盖才是灾难。 ## 建档不再自动铺 seedPetDefaults 只建必要提醒(疫苗、驱虫)。漏针是真会出事的,而且它们是 「到期提醒」不占计划页的格子。其余交给用户挑方案。 自动铺的问题是:用户还没看懂这个 App 是干什么的,首页已经躺着十几条他没 答应过的任务,第一反应是「这些是什么」而不是「我该做什么」。 **改完发现还有第二条自动铺的路径**:newStagePlan 在建计划壳子时顺手把模板 铺成节点。所以我改完 seedPetDefaults 之后,建档后首页仍然躺着 5 条任务。 惰性生成尤其难查——不是建档时发生的,是第一次读计划才发生的。 ## 方案摘要按重复方式分开报数 一开始只报一个总数,「10 条」套用一整月展开成 130 条,用户会以为出 bug。 现在分成 每天 4 · 每周 0 · 每月 0 · 一次性 6 · 提醒 4。 ## 数据迁移(预生产库,已执行) 模板明细 kind=task → plan + repeat=daily 28 条 模板明细 kind=plan 补 repeat=once 44 条 8 套方案补名字和摘要 DailyTask → PlanTask 34 条,跳过 0 DailyTask 表的行**没删**,先留着当备份。 迁移第一版又踩了「列还没 AutoMigrate 出来就跑」那个坑:Count 静默返回 0, 我以为「没有需要补的」。第二版加了 HasColumn 前置检查才发现 44 条没转。 和上次 record_types.fields 回填是同一个错。 ## 验证(预生产库实跑) 今日任务/计划页今天 4 条 vs 4 条,一致 建档后 0 任务 / 0 计划节点 / 4 条必要提醒 套用本月 从今天到月末,4 条(7/30-7/31 两天) 重复套用 0 条新增(按「日期+标题」去重) 套用下月 130 条覆盖 31 天,每天 4.2 条 全部完成 5 → 0 未做 ## 还没做(下一步) 前端:建档第 4 步选方案、计划页空月份的套用入口、首页「管理」改跳计划页、 弹层删 manageTasks;后台模板页要支持多套 + 重复规则编辑。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
毛孩子计划 · 后端 (pets-be)
Gin + GORM + MySQL + MinIO 的宠物小程序后端,内嵌 React(Vite + Tailwind + shadcn/ui) 后台管理。
技术栈与约定
- 模块路径
github.com/sundynix/pets-be,标准 Go 布局(cmd/ internal/ pkg/ configs/ deployments/ web/) - 所有数据库表带前缀
sundynix_ - 统一响应
{code, message, data}(pkg/response),列表接口统一分页参数page/page_size(response.PageQuery+PageResult) - MySQL
root/root库pets;MinIOsundynix/sundynix桶pets - 服务端口
:8080;后台在http://localhost:8080/admin(seed 管理员sundynix/sundynix)
快速开始
# 1. 起依赖(MySQL + MinIO)
make env-up # 或 cd deployments && docker compose up -d
# 2. 构建后台前端 + 后端单二进制
make build # 产物 bin/pets-be(已 embed 后台)
./bin/pets-be
# 或开发期直接运行(使用现有 internal/admin/dist)
make run
后台前端开发(热更新)
make admin-dev # vite dev server :5173,已代理 /api → :8080
# 改完执行 make admin-build 让产物落到 internal/admin/dist,再 go build 即 embed
目录
cmd/server入口:载配置→连库→迁移→seed→建桶→路由→启动internal/config配置(viper,支持PETS_环境变量覆盖)internal/modelGORM 模型(sundynix_前缀)internal/database连接/迁移/seedinternal/storageMinIO 封装(自动建桶 + 上传)internal/service业务逻辑;internal/handlerHTTP 处理器;internal/router路由internal/middlewareCORS / 用户鉴权 / 管理员鉴权internal/admingo:embed内嵌后台产物 + SPA fallbackpkg/response统一响应与分页;pkg/jwt令牌;pkg/errcode错误码web/admin后台前端源码
接口
- 小程序
/api/*(用户 JWT):auth / user / pets / onboarding / records / tasks / plan / reminders / report / bill / community / articles / pro / ai / upload - 后台
/api/admin/*(管理员 JWT):login / stats / users / pets / posts / comments / articles / memberships
登录鉴权
默认 auth.dev_login: true,POST /api/auth/login {"nickname":"xxx"} 即发用户 JWT。
接真微信:在 configs/config.yaml 填 wechat.app_secret 并把 dev_login 置 false,实现 /api/auth/wechat(code2session)。
access token + refresh token
登录接口返回 token(access) 与 refresh_token 两个令牌:
- access token:JWT,默认 2 小时(
jwt.expire_hours),放Authorization: Bearer,服务端无状态校验。 - refresh token:随机串,落库
sundynix_refresh_tokens(只存 sha256)。小程序 30 天、后台 7 天。 - 续期:
POST /api/auth/refresh/POST /api/admin/refresh,入参{"refresh_token":"..."},返回新的一对令牌。 - 退出:
POST /api/auth/logout/POST /api/admin/logout作废该刷新令牌。
安全约定:
- 每次续期都轮换刷新令牌,旧的立即作废;作废后 60 秒内再到达算并发重试(放行),超过则判定泄露,吊销该账号全部会话。
- 主动退出、被连坐吊销的令牌不享受宽限期。
- 禁用用户时一并吊销其刷新令牌,最多 2 小时后彻底失去访问。
前端两端都做了单飞续期:并发请求同时 401 只会发一次 refresh(否则刷新令牌会被并发轮换掉)。小程序续期失败会回退到 wx.login 重新登录。