feat: 为用户设备添加 DeviceNo 字段并实现 ID 混淆,同时在迁移脚本中新增 ID 重建逻辑。
Some checks failed
Build docker and publish / build (20.15.1) (push) Has been cancelled

This commit is contained in:
shanshanzhong 2026-03-15 19:38:37 -07:00
parent 3db14ae472
commit 3d732c0361
9 changed files with 274 additions and 10 deletions

View File

@ -545,6 +545,7 @@ type (
Id int64 `json:"id"`
Ip string `json:"ip"`
Identifier string `json:"identifier"`
DeviceNo string `json:"device_no"`
UserAgent string `json:"user_agent"`
Online bool `json:"online"`
Enabled bool `json:"enabled"`

View File

@ -35,6 +35,11 @@ func (l *GetUserDetailLogic) GetUserDetail(req *types.GetDetailRequest) (*types.
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get user detail error: %v", err.Error())
}
tool.DeepCopy(&resp, userInfo)
for i, d := range userInfo.UserDevices {
if i < len(resp.UserDevices) {
resp.UserDevices[i].DeviceNo = tool.DeviceIdToHash(d.Id)
}
}
if referCode := strings.TrimSpace(resp.ReferCode); referCode != "" {
resp.ShareLink = logicCommon.NewInviteLinkResolver(l.ctx, l.svcCtx).ResolveInviteLink(referCode)
}

View File

@ -163,6 +163,13 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge
}
u.AuthMethods = authMethods
// 填充 DeviceNo
for i, d := range item.UserDevices {
if i < len(u.UserDevices) {
u.UserDevices[i].DeviceNo = tool.DeviceIdToHash(d.Id)
}
}
if relation, ok := relationMap[item.Id]; ok {
u.FamilyJoined = true
u.FamilyId = relation.FamilyId

View File

@ -34,8 +34,14 @@ func (l *GetUserSubscribeDevicesLogic) GetUserSubscribeDevices(req *types.GetUse
}
userRespList := make([]types.UserDevice, 0)
tool.DeepCopy(&userRespList, list)
for i, d := range list {
if i < len(userRespList) {
userRespList[i].DeviceNo = tool.DeviceIdToHash(d.Id)
}
}
return &types.GetUserSubscribeDevicesResponse{
Total: total,
List: userRespList,
}, nil
}

View File

@ -36,6 +36,11 @@ func (l *GetDeviceListLogic) GetDeviceList() (resp *types.GetDeviceListResponse,
list, count, err := l.svcCtx.UserModel.QueryDeviceListByUserIds(l.ctx, scopeUserIds)
userRespList := make([]types.UserDevice, 0)
tool.DeepCopy(&userRespList, list)
for i, d := range list {
if i < len(userRespList) {
userRespList[i].DeviceNo = tool.DeviceIdToHash(d.Id)
}
}
resp = &types.GetDeviceListResponse{
Total: count,
List: userRespList,

View File

@ -44,6 +44,11 @@ func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) {
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
}
tool.DeepCopy(resp, u)
for i, d := range u.UserDevices {
if i < len(resp.UserDevices) {
resp.UserDevices[i].DeviceNo = tool.DeviceIdToHash(d.Id)
}
}
ownerEmailMethod := l.fillFamilyContext(resp, u.Id)
var userMethods []types.UserAuthMethod

View File

@ -2956,6 +2956,7 @@ type UserDevice struct {
Id int64 `json:"id"`
Ip string `json:"ip"`
Identifier string `json:"identifier"`
DeviceNo string `json:"device_no"`
UserAgent string `json:"user_agent"`
Online bool `json:"online"`
Enabled bool `json:"enabled"`

25
pkg/tool/device.go Normal file
View File

@ -0,0 +1,25 @@
package tool
import (
"fmt"
"strings"
)
const deviceHashSalt uint32 = 0x5A3C7E9B
// DeviceIdToHash encodes a device id to an 8-char uppercase hex string.
// Algorithm mirrors frontend: id XOR salt → hex.
// e.g. 1 → "5A3C7E9A", 42 → "5A3C7EA1"
func DeviceIdToHash(id int64) string {
return strings.ToUpper(fmt.Sprintf("%08x", uint32(id)^deviceHashSalt))
}
// HashToDeviceId decodes an 8-char hex hash back to a device id.
func HashToDeviceId(hash string) (int64, error) {
var n uint32
_, err := fmt.Sscanf(strings.ToLower(hash), "%08x", &n)
if err != nil {
return 0, err
}
return int64(n ^ deviceHashSalt), nil
}

View File

@ -253,7 +253,7 @@ func main() {
// 连接源库
fmt.Println("=== 付费用户数据迁移 ===")
fmt.Println()
fmt.Print("[1/9] 连接源数据库... ")
fmt.Print("[1/10] 连接源数据库... ")
srcDB, err := gorm.Open(mysql.Open(*srcDSN), gormCfg)
if err != nil {
log.Fatalf("源库连接失败: %v", err)
@ -261,7 +261,7 @@ func main() {
fmt.Println("OK")
// 连接目标库
fmt.Print("[2/9] 连接目标数据库... ")
fmt.Print("[2/10] 连接目标数据库... ")
dstDB, err := gorm.Open(mysql.Open(*dstDSN), gormCfg)
if err != nil {
log.Fatalf("目标库连接失败: %v", err)
@ -269,7 +269,7 @@ func main() {
fmt.Println("OK")
// ── Step 3: 查询付费用户 ID ──
fmt.Print("[3/9] 查询付费用户... ")
fmt.Print("[3/10] 查询付费用户... ")
var paidIDs []int64
err = srcDB.Raw(`
SELECT DISTINCT t.uid FROM (
@ -291,7 +291,7 @@ func main() {
}
// ── Step 4: 读取源库数据 ──
fmt.Print("[4/9] 读取源库数据... ")
fmt.Print("[4/10] 读取源库数据... ")
var (
users []User
auths []AuthMethod
@ -374,6 +374,186 @@ func main() {
fmt.Printf("\n 多设备拆分: %d 个设备 → 独立用户\n", len(splits))
fmt.Println()
// ── ID 重建:将所有记录主键从 1 开始连续重赋值,并同步更新所有外键 ──
fmt.Println(" 重建 ID...")
// 各表 old→new 映射(仅对有外键引用的表建立映射)
userIDMap := make(map[int64]int64, len(users))
orderIDMap := make(map[int64]int64, len(orders))
subPlanIDMap := make(map[int64]int64, len(subPlans))
paymentIDMap := make(map[int64]int64, len(payments))
deviceIDMap := make(map[int64]int64, len(devices))
// 1. 重建 user ID从 1 开始连续)
for i := range users {
newID := int64(i + 1)
userIDMap[users[i].Id] = newID
users[i].Id = newID
}
// 2. 重建 subscribe 配置表 ID
for i := range subPlans {
newID := int64(i + 1)
subPlanIDMap[subPlans[i].Id] = newID
subPlans[i].Id = newID
}
// 3. 重建 payment 配置表 ID
for i := range payments {
newID := int64(i + 1)
paymentIDMap[payments[i].Id] = newID
payments[i].Id = newID
}
// 4. 重建 auth_methods ID + 更新 user_id 外键
for i := range auths {
auths[i].Id = int64(i + 1)
if v, ok := userIDMap[auths[i].UserId]; ok {
auths[i].UserId = v
}
}
// 5. 重建 device ID + 更新 user_id 外键
for i := range devices {
newID := int64(i + 1)
deviceIDMap[devices[i].Id] = newID
devices[i].Id = newID
if v, ok := userIDMap[devices[i].UserId]; ok {
devices[i].UserId = v
}
}
// 6. 重建 order ID + 外键user_id / subscription_user_id / payment_id / subscribe_id
for i := range orders {
newID := int64(i + 1)
orderIDMap[orders[i].Id] = newID
orders[i].Id = newID
if v, ok := userIDMap[orders[i].UserId]; ok {
orders[i].UserId = v
}
if orders[i].SubscriptionUserId > 0 {
if v, ok := userIDMap[orders[i].SubscriptionUserId]; ok {
orders[i].SubscriptionUserId = v
}
}
if orders[i].PaymentId > 0 {
if v, ok := paymentIDMap[orders[i].PaymentId]; ok {
orders[i].PaymentId = v
}
}
if orders[i].SubscribeId > 0 {
if v, ok := subPlanIDMap[orders[i].SubscribeId]; ok {
orders[i].SubscribeId = v
}
}
}
// 二次处理 order.ParentId父子订单指向同表需在 orderIDMap 完整建立后再处理)
for i := range orders {
if orders[i].ParentId > 0 {
if v, ok := orderIDMap[orders[i].ParentId]; ok {
orders[i].ParentId = v
}
}
}
// 7. 重建 user_subscribe ID + 外键
for i := range subscribes {
subscribes[i].Id = int64(i + 1)
if v, ok := userIDMap[subscribes[i].UserId]; ok {
subscribes[i].UserId = v
}
if subscribes[i].OrderId > 0 {
if v, ok := orderIDMap[subscribes[i].OrderId]; ok {
subscribes[i].OrderId = v
}
}
if subscribes[i].SubscribeId > 0 {
if v, ok := subPlanIDMap[subscribes[i].SubscribeId]; ok {
subscribes[i].SubscribeId = v
}
}
}
// 8. 重建 iap ID + 更新 user_id 外键
for i := range iaps {
iaps[i].Id = int64(i + 1)
if v, ok := userIDMap[iaps[i].UserId]; ok {
iaps[i].UserId = v
}
}
// 9. 更新 paidIDsStep 8 家庭组创建使用新 user ID
for i, uid := range paidIDs {
if v, ok := userIDMap[uid]; ok {
paidIDs[i] = v
}
}
// 10. 更新 splits 中的 OwnerUID 和 Device 副本
// Device 是值拷贝,需通过 deviceIDMap 单独更新AuthMethod 是指针,已随 auths[i] 同步
for i := range splits {
if v, ok := userIDMap[splits[i].OwnerUID]; ok {
splits[i].OwnerUID = v
}
if v, ok := deviceIDMap[splits[i].Device.Id]; ok {
splits[i].Device.Id = v
}
if v, ok := userIDMap[splits[i].Device.UserId]; ok {
splits[i].Device.UserId = v
}
}
fmt.Printf(" OK — user:%d auth:%d device:%d order:%d subscribe:%d iap:%d\n",
len(userIDMap), len(auths), len(deviceIDMap), len(orderIDMap), len(subscribes), len(iaps))
// ── 注入默认管理员用户devneeds52@gmail.com ──
// 该用户不涉及付费订单ID 紧接在已迁移用户之后,避免冲突
{
defaultCreatedAt := time.Date(2025, 9, 30, 9, 33, 45, 780_000_000, time.UTC)
lastLogin := time.Date(2026, 3, 15, 17, 13, 45, 0, time.UTC)
defaultUID := int64(len(users) + 1)
defaultUser := User{
Id: defaultUID,
Password: "$pbkdf2-sha512$kyFSMS4eAnupW7bX$38953ce0e7ec8415c39603bdc3010050ddab2e433f0383222215bbec013450e3",
Algo: "default",
Salt: "default",
Avatar: "",
Balance: 0,
ReferCode: "uuEPXVjS",
Commission: 0,
ReferralPercentage: 0,
OnlyFirstPurchase: boolPtr(true),
GiftAmount: 0,
Enable: boolPtr(true),
IsAdmin: boolPtr(true),
EnableBalanceNotify: boolPtr(false),
EnableLoginNotify: boolPtr(false),
EnableSubscribeNotify: boolPtr(false),
EnableTradeNotify: boolPtr(false),
LastLoginTime: &lastLogin,
MemberStatus: "",
Remark: "",
CreatedAt: defaultCreatedAt,
UpdatedAt: time.Now(),
}
users = append(users, defaultUser)
defaultAuth := AuthMethod{
Id: int64(len(auths) + 1),
UserId: defaultUID,
AuthType: "email",
AuthIdentifier: "devneeds52@gmail.com",
Verified: boolPtr(true),
CreatedAt: defaultCreatedAt,
UpdatedAt: defaultCreatedAt,
}
auths = append(auths, defaultAuth)
fmt.Printf(" 注入管理员: uid=%d email=devneeds52@gmail.com\n", defaultUID)
}
fmt.Println()
if *dryRun {
fmt.Println("[DRY-RUN] 仅分析,不写入目标库")
return
@ -381,7 +561,7 @@ func main() {
// ── Step 5: 清空目标库(可选) ──
if *clean {
fmt.Print("[5/9] 清空目标库... ")
fmt.Print("[5/10] 清空目标库... ")
dstDB.Exec("SET FOREIGN_KEY_CHECKS = 0")
for _, tbl := range []string{
"user", "user_auth_methods", "user_device",
@ -393,11 +573,11 @@ func main() {
dstDB.Exec("SET FOREIGN_KEY_CHECKS = 1")
fmt.Println("OK")
} else {
fmt.Println("[5/9] 跳过清空(未指定 -clean")
fmt.Println("[5/10] 跳过清空(未指定 -clean")
}
// ── Step 6: 写入全量配置表 ──
fmt.Print("[6/9] 写入全量配置表... ")
fmt.Print("[6/10] 写入全量配置表... ")
if len(subPlans) > 0 {
dstDB.Exec("DELETE FROM subscribe") // 先清再插
for _, s := range subPlans {
@ -413,7 +593,7 @@ func main() {
fmt.Println("OK")
// ── Step 7: 写入付费用户数据(事务) ──
fmt.Print("[7/9] 写入付费用户数据... ")
fmt.Print("[7/10] 写入付费用户数据... ")
err = dstDB.Transaction(func(tx *gorm.DB) error {
tx.Exec("SET FOREIGN_KEY_CHECKS = 0")
@ -445,7 +625,7 @@ func main() {
fmt.Println("OK")
// ── Step 8: 创建家庭组 ──
fmt.Print("[8/9] 创建家庭组... ")
fmt.Print("[8/10] 创建家庭组... ")
now := time.Now()
familyCount := 0
@ -485,7 +665,7 @@ func main() {
fmt.Printf("%d 个家庭组\n", familyCount)
// ── Step 9: 多设备拆分 ──
fmt.Print("[9/9] 多设备拆分... ")
fmt.Print("[9/10] 多设备拆分... ")
splitCount := 0
err = dstDB.Transaction(func(tx *gorm.DB) error {
@ -553,6 +733,35 @@ func main() {
}
fmt.Printf("%d 个设备\n", splitCount)
// ── Step 10: 修复各表 AUTO_INCREMENT ──
// 确保迁移后新写入的记录不会触发主键冲突
fmt.Print("[10/10] 修复 AUTO_INCREMENT... ")
type autoIncTable struct {
table string // 表名(不含反引号)
quoted string // SQL 中使用的表名(含反引号)
}
autoIncTables := []autoIncTable{
{"user", "`user`"},
{"user_auth_methods", "`user_auth_methods`"},
{"user_device", "`user_device`"},
{"order", "`order`"},
{"user_subscribe", "`user_subscribe`"},
{"apple_iap_transactions", "`apple_iap_transactions`"},
{"user_family", "`user_family`"},
{"user_family_member", "`user_family_member`"},
}
for _, t := range autoIncTables {
var maxID int64
dstDB.Raw(fmt.Sprintf("SELECT COALESCE(MAX(id), 0) FROM %s", t.quoted)).Scan(&maxID)
nextID := maxID + 1
if err := dstDB.Exec(fmt.Sprintf("ALTER TABLE %s AUTO_INCREMENT = %d", t.quoted, nextID)).Error; err != nil {
log.Printf(" 警告: 修复 %s AUTO_INCREMENT 失败: %v", t.table, err)
} else {
fmt.Printf("\n %-30s MAX(id)=%-8d AUTO_INCREMENT→%d", t.table, maxID, nextID)
}
}
fmt.Println("\nOK")
// ── 结果 ──
fmt.Println()
fmt.Println("=== 迁移完成 ===")