package ingest import ( "crypto/sha256" "encoding/binary" "strings" ) // 本文件是 backend_v2/internal/pkg/hashid 中「无类型编码(品牌等)」的忠实移植, // 仅用于 spider 离线 Mock 模式下生成 brand_uid(真实上报路径下 brand_uid 直接来自后台接口,无需自编码)。 // 算法:SHA256(盐) 派生 4 个子密钥 → 32-bit 平衡 Feistel 网络 → base62 定长(minLen=8) 序列化。 // 默认盐与后台一致:hashid_secret 为空时后台走 "fashion-archive-default-salt-change-me"。 const ( hidAlphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" hidBase = 62 hidMinLen = 8 ) var hidKeys [4]uint32 // InitHashID 用部署级盐值初始化混淆密钥。盐为空时使用与后台相同的内置默认盐。 func InitHashID(secret string) { if secret == "" { secret = "fashion-archive-default-salt-change-me" } h := sha256.Sum256([]byte(secret)) for i := 0; i < 4; i++ { hidKeys[i] = binary.BigEndian.Uint32(h[i*4 : i*4+4]) } } // feistel 32-bit 平衡 Feistel 网络(全局密钥版,品牌等无类型实体用)。 func feistel(v uint32, encrypt bool) uint32 { return feistelK(v, encrypt, hidKeys) } func feistelK(v uint32, encrypt bool, k [4]uint32) uint32 { const rounds = 8 l, r := uint16(v>>16), uint16(v&0xffff) for i := 0; i < rounds; i++ { idx := i if !encrypt { idx = rounds - 1 - i } round := func(h uint16) uint16 { f := uint32(h)*0x9E3779B1 + k[idx%4] return uint16((f ^ (f >> 16)) & 0xffff) } if encrypt { nl := r nr := l ^ round(r) l, r = nl, nr } else { nl := r ^ round(l) nr := l l, r = nl, nr } } return uint32(l)<<16 | uint32(r) } // numToBase62 把 32-bit 值序列化为定长(minLen)base62 串,高位在前。 func numToBase62(x uint32) string { var sb strings.Builder for x > 0 { sb.WriteByte(hidAlphabet[x%hidBase]) x /= hidBase } if sb.Len() == 0 { sb.WriteByte(hidAlphabet[0]) } runes := []rune(sb.String()) for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 { runes[i], runes[j] = runes[j], runes[i] } out := string(runes) if len(out) < hidMinLen { out = strings.Repeat("0", hidMinLen-len(out)) + out } return out } // EncodeBrand 把数字品牌主键编码为对外 hashid 串(与后台 hashid.Encode 同算法、同盐)。 func EncodeBrand(id uint32) string { return numToBase62(feistel(id, true)) }