package service import ( "log" "strings" "time" "github.com/sundynix/pets-be/internal/model" ) // IDCard 宠物身份卡的全部内容,前端拿去 canvas 上画 type IDCard struct { Name string `json:"name"` Species string `json:"species"` // 猫猫 / 狗狗 Gender string `json:"gender"` Breed string `json:"breed"` Birthday string `json:"birthday"` // "2023 年 12 月 15 日" Age string `json:"age"` // 年龄,如 "2岁7个月" Days int `json:"days"` // 一起生活的天数(按到家日) Zodiac string `json:"zodiac"` // 星座,按生日算 Weight string `json:"weight"` // 体重,如 "13.5kg" Intro string `json:"intro"` Color string `json:"color"` // 毛色 Personality string `json:"personality"` // 性格标签,逗号分隔 AvatarURL string `json:"avatar_url"` IDNo string `json:"id_no"` // 玩具身份号,不入库 Validity string `json:"validity"` // "2023.12.15 - 永远" // —— 背面:照护信息 —— Owner string `json:"owner"` // 主人昵称 Phone string `json:"phone"` // 联系电话(脱敏) Allergy string `json:"allergy"` // 过敏情况 Notes string `json:"notes"` // 备注 VaccineState string `json:"vaccine_state"` // 疫苗情况 DewormState string `json:"deworm_state"` // 驱虫情况 Org string `json:"org"` // 发证机构 QRURL string `json:"qr_url"` // 小程序码,未发布时为空 QRReady bool `json:"qr_ready"` // 码有没有生成出来 } // petIDNo 造一个稳定的「身份号」。纯装饰,不入库、不做校验。 // 规律照竞品那种:一串数字,扫不出真实含义但看着像证件号。 // 用 宠物雪花 id 的数字 + 生日 拼,同一只宠物每次都一样 func petIDNo(pet *model.Pet) string { digits := make([]byte, 0, 18) for i := 0; i < len(pet.ID) && len(digits) < 10; i++ { if pet.ID[i] >= '0' && pet.ID[i] <= '9' { digits = append(digits, pet.ID[i]) } } for len(digits) < 10 { digits = append(digits, '0') } bd := "00000000" if pet.Birthday != nil && !pet.Birthday.IsZero() { bd = pet.Birthday.Format("20060102") } raw := string(digits[:10]) + bd // 18 位 // 每 4 位空一下,像证件号 var b strings.Builder for i, c := range raw { if i > 0 && i%4 == 0 { b.WriteByte(' ') } b.WriteRune(c) } return b.String() } // zodiacOf 按月日算西方星座。按各星座起始日(含)顺序判定, // 落在 12/22–1/19 的默认摩羯座 func zodiacOf(month, day int) string { starts := []struct { m, d int name string }{ {1, 20, "水瓶座"}, {2, 19, "双鱼座"}, {3, 21, "白羊座"}, {4, 20, "金牛座"}, {5, 21, "双子座"}, {6, 22, "巨蟹座"}, {7, 23, "狮子座"}, {8, 23, "处女座"}, {9, 23, "天秤座"}, {10, 24, "天蝎座"}, {11, 22, "射手座"}, {12, 22, "摩羯座"}, } name := "摩羯座" for _, s := range starts { if month > s.m || (month == s.m && day >= s.d) { name = s.name } } return name } // maskPhone 脱敏手机号:138****8888。空或非 11 位原样返回 func maskPhone(p string) string { if len(p) != 11 { return p } return p[:3] + "****" + p[7:] } // GetIDCard 组装身份卡。小程序码是 best-effort:拿不到(未发布/配额)也照出卡 func (s *Service) GetIDCard(userID, petID string) (*IDCard, error) { pet, err := s.GetPet(userID, petID) if err != nil { return nil, err } intro := "地球来了个新居民~" switch pet.Type { case "狗狗": intro = "汪星人来地球啦~" case "猫猫": intro = "喵星人来地球啦~" } bd, validity := "未知", "永久有效" if pet.Birthday != nil && !pet.Birthday.IsZero() { bd = pet.Birthday.Format("2006 年 1 月 2 日") validity = pet.Birthday.Format("2006.01.02") + " - 永远" } zodiac := "" if pet.Birthday != nil && !pet.Birthday.IsZero() { zodiac = zodiacOf(int(pet.Birthday.Month()), pet.Birthday.Day()) } // 主人昵称 + 脱敏电话 var user model.User s.db.First(&user, userID) owner := user.Nickname if owner == "" { owner = "铲屎官" } // 疫苗:有记录算已接种;驱虫:取下一次到期提醒的日期 var vaccineCnt int64 s.db.Model(&model.HealthRecord{}).Where("pet_id = ? AND type = ?", petID, model.RecordVaccine).Count(&vaccineCnt) vaccineState := "待接种" if vaccineCnt > 0 { vaccineState = "已接种" } dewormState := "—" var dw model.Reminder if err := s.db.Where("pet_id = ? AND type = ?", petID, model.ReminderDeworm).Order("next_due_date asc").First(&dw).Error; err == nil && dw.NextDueDate != nil { dewormState = dw.NextDueDate.Format("2006.01.02") } allergy := pet.Allergy if allergy == "" { allergy = "无" } // 一起生活的天数:按到家日算,没填就 0 days := 0 if pet.ArrivedAt != nil && !pet.ArrivedAt.IsZero() { days = int(time.Since(*pet.ArrivedAt).Hours()/24) + 1 } card := &IDCard{ Name: pet.Name, Species: pet.Type, Gender: pet.Gender, Breed: pet.Breed, Birthday: bd, Age: pet.Age, Days: days, Zodiac: zodiac, Weight: pet.Weight, Intro: intro, Color: pet.Color, Personality: pet.Personality, AvatarURL: pet.AvatarURL, IDNo: petIDNo(pet), Validity: validity, Owner: owner, Phone: maskPhone(user.Phone), Allergy: allergy, Notes: pet.Notes, VaccineState: vaccineState, DewormState: dewormState, Org: "肉垫计划", } // 小程序码取不到不算错——未发布/配额用尽时卡照样出,只是没码 if url, e := s.PetQRCode(petID); e == nil { card.QRURL = url card.QRReady = true } else { log.Printf("[info] 宠物 %s 的小程序码暂不可用(多为小程序未发布):%v", petID, e) } return card, nil }