59412010dc
- 修正性别 ♂/♀:用名字字号量宽度再定位,不再怼到名字上(之前用小字号量偏了) - 正反两张卡右上角各盖一枚「肉垫计划 · 官方认证」红色公章(双圈+五角星,微旋转半透明) - 照护卡标题改成两行「X星地球移民 / 居民身份卡」(按猫狗取 喵/汪/宠) - 正面「物ID」改为「ID」 - 后端 IDCard.Phone 返回完整手机号,不再脱敏 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
175 lines
5.6 KiB
Go
175 lines
5.6 KiB
Go
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: 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
|
||
}
|