feat: 为用户设备添加 DeviceNo 字段并实现 ID 混淆,同时在迁移脚本中新增 ID 重建逻辑。
Some checks failed
Build docker and publish / build (20.15.1) (push) Has been cancelled
Some checks failed
Build docker and publish / build (20.15.1) (push) Has been cancelled
This commit is contained in:
parent
3db14ae472
commit
3d732c0361
@ -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"`
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
25
pkg/tool/device.go
Normal 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
|
||||
}
|
||||
@ -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. 更新 paidIDs(Step 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("=== 迁移完成 ===")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user