Files
sundynix-pets/pets-be/internal/service/idcard.go
T
Blizzard 586875f8c5 feat: 身份卡改上下两张卡(正面/背面) + 手机号硬门槛
- canvas 重画成上下两张:正面(暖黄,头像/名字性别/性别品种毛色年龄/性格
  彩色标签/介绍/物ID+一起生活天数)、背面(蓝色照护信息条 + 主人/电话/疫苗/
  备注/发证机构/有效期 + 走失联系框&小程序码)
- 生成前查手机号:没有则唤起 getPhoneNumber 组件,验证成功才生成;
  拒绝则提示「无法生成」不出卡
- IDCard 增 age/days;ic-canvas 高度改 1480 否则导出被裁

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 17:48:39 +08:00

175 lines
5.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/221/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
}