package memory import ( "strings" "time" "yunyan/comm" "yunyan/pb" ) // 分页与范围上限。放常量而不是散在各处:这些值同时约束着「数据导出拉全量」 // 与「月视图一次拉一个月」两种用法,改一处即可。 const ( defaultListSize = 200 maxListSize = 1000 maxUpcomingDays = 30 maxTitleLen = 200 maxBatchIDs = 200 ) // requireUID 取会话 uid。所有接口的第一步。 func requireUID(session comm.IUserSession) (string, *pb.ErrorData) { uid := session.GetUserId() if uid == "" { return "", &pb.ErrorData{Code: pb.ErrorCode_NoLogin, Message: pb.ErrorCode_NoLogin.String()} } return uid, nil } // normalizeItem 校验并规整一条待写入的记忆项。 // // 客户端传上来的 item 里,凡是不该由客户端决定的字段一律在这里覆盖掉 // (uid/id/create_time/gen_round/source_id…),不要指望调用方老实。 func normalizeItem(uid string, item *pb.DBMemoryItem) *pb.ErrorData { if item == nil { return &pb.ErrorData{Code: pb.ErrorCode_MemoryParamInvalid, Message: "item 为空"} } item.Category = strings.ToLower(strings.TrimSpace(item.Category)) if item.Category == "" { item.Category = comm.MemoryCatTodo } if !comm.IsValidMemoryCategory(item.Category) { return &pb.ErrorData{Code: pb.ErrorCode_MemoryCategoryInvalid, Message: "不支持的分类: " + item.Category} } item.Title = strings.TrimSpace(item.Title) if item.Title == "" { return &pb.ErrorData{Code: pb.ErrorCode_MemoryParamInvalid, Message: "标题不能为空"} } if len([]rune(item.Title)) > maxTitleLen { item.Title = string([]rune(item.Title)[:maxTitleLen]) } // happen_date 永不为空——这是拾忆页能工作的前提,日历按日期排, // 没有日期的项根本渲染不出来。客户端没给就落在它自己的「今天」。 item.HappenDate = strings.TrimSpace(item.HappenDate) if item.HappenDate == "" { loc := comm.LoadMemoryLocation(item.Tz) item.HappenDate = comm.FormatMemoryDate(time.Now().In(loc)) item.DateCertain = false } else if _, ok := comm.ParseMemoryDate(item.HappenDate); !ok { return &pb.ErrorData{Code: pb.ErrorCode_MemoryParamInvalid, Message: "happen_date 格式应为 YYYY-MM-DD"} } if item.HappenTime != "" { if _, _, ok := comm.ParseMemoryClock(item.HappenTime); !ok { return &pb.ErrorData{Code: pb.ErrorCode_MemoryParamInvalid, Message: "happen_time 格式应为 HH:mm"} } } if !comm.IsValidMemoryRepeat(item.RepeatRule) { return &pb.ErrorData{Code: pb.ErrorCode_MemoryParamInvalid, Message: "不支持的重复规则: " + item.RepeatRule} } if item.RepeatRule == comm.MemoryRepeatWeekly && (item.Weekday < 1 || item.Weekday > 7) { // 没给星期几就按 happen_date 那天推出来,别让它落成 0 永远匹配不上 if d, ok := comm.ParseMemoryDate(item.HappenDate); ok { w := int32(d.Weekday()) if w == 0 { w = 7 } item.Weekday = w } else { item.Weekday = 1 } } if item.AmountCents < 0 { return &pb.ErrorData{Code: pb.ErrorCode_MemoryParamInvalid, Message: "金额不能为负"} } if item.Currency == "" { item.Currency = "CNY" } if item.RemindAhead < 0 { item.RemindAhead = 0 } if item.Source == "" { item.Source = comm.MemorySrcManual } // client_key 服务端保证非空:它参与 (uid, client_key) 唯一索引,而 MySQL 的 // 唯一索引不像放行 NULL 那样放行空串,留空会让同一用户的第二条记录插不进去。 if strings.TrimSpace(item.ClientKey) == "" { item.ClientKey = comm.NewMemoryClientKey() } item.Uid = uid return nil } // clampListSize 分页尺寸夹取 func clampListSize(size int32) int32 { if size <= 0 { return defaultListSize } if size > maxListSize { return maxListSize } return size } // clampIDs 批量操作的 id 数量夹取,顺带去重 func clampIDs(ids []uint64) []uint64 { seen := map[uint64]bool{} out := make([]uint64, 0, len(ids)) for _, id := range ids { if id == 0 || seen[id] { continue } seen[id] = true out = append(out, id) if len(out) >= maxBatchIDs { break } } return out }