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"`
|
Id int64 `json:"id"`
|
||||||
Ip string `json:"ip"`
|
Ip string `json:"ip"`
|
||||||
Identifier string `json:"identifier"`
|
Identifier string `json:"identifier"`
|
||||||
|
DeviceNo string `json:"device_no"`
|
||||||
UserAgent string `json:"user_agent"`
|
UserAgent string `json:"user_agent"`
|
||||||
Online bool `json:"online"`
|
Online bool `json:"online"`
|
||||||
Enabled bool `json:"enabled"`
|
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())
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "get user detail error: %v", err.Error())
|
||||||
}
|
}
|
||||||
tool.DeepCopy(&resp, userInfo)
|
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 != "" {
|
if referCode := strings.TrimSpace(resp.ReferCode); referCode != "" {
|
||||||
resp.ShareLink = logicCommon.NewInviteLinkResolver(l.ctx, l.svcCtx).ResolveInviteLink(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
|
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 {
|
if relation, ok := relationMap[item.Id]; ok {
|
||||||
u.FamilyJoined = true
|
u.FamilyJoined = true
|
||||||
u.FamilyId = relation.FamilyId
|
u.FamilyId = relation.FamilyId
|
||||||
|
|||||||
@ -34,8 +34,14 @@ func (l *GetUserSubscribeDevicesLogic) GetUserSubscribeDevices(req *types.GetUse
|
|||||||
}
|
}
|
||||||
userRespList := make([]types.UserDevice, 0)
|
userRespList := make([]types.UserDevice, 0)
|
||||||
tool.DeepCopy(&userRespList, list)
|
tool.DeepCopy(&userRespList, list)
|
||||||
|
for i, d := range list {
|
||||||
|
if i < len(userRespList) {
|
||||||
|
userRespList[i].DeviceNo = tool.DeviceIdToHash(d.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
return &types.GetUserSubscribeDevicesResponse{
|
return &types.GetUserSubscribeDevicesResponse{
|
||||||
Total: total,
|
Total: total,
|
||||||
List: userRespList,
|
List: userRespList,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,6 +36,11 @@ func (l *GetDeviceListLogic) GetDeviceList() (resp *types.GetDeviceListResponse,
|
|||||||
list, count, err := l.svcCtx.UserModel.QueryDeviceListByUserIds(l.ctx, scopeUserIds)
|
list, count, err := l.svcCtx.UserModel.QueryDeviceListByUserIds(l.ctx, scopeUserIds)
|
||||||
userRespList := make([]types.UserDevice, 0)
|
userRespList := make([]types.UserDevice, 0)
|
||||||
tool.DeepCopy(&userRespList, list)
|
tool.DeepCopy(&userRespList, list)
|
||||||
|
for i, d := range list {
|
||||||
|
if i < len(userRespList) {
|
||||||
|
userRespList[i].DeviceNo = tool.DeviceIdToHash(d.Id)
|
||||||
|
}
|
||||||
|
}
|
||||||
resp = &types.GetDeviceListResponse{
|
resp = &types.GetDeviceListResponse{
|
||||||
Total: count,
|
Total: count,
|
||||||
List: userRespList,
|
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")
|
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||||
}
|
}
|
||||||
tool.DeepCopy(resp, u)
|
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)
|
ownerEmailMethod := l.fillFamilyContext(resp, u.Id)
|
||||||
|
|
||||||
var userMethods []types.UserAuthMethod
|
var userMethods []types.UserAuthMethod
|
||||||
|
|||||||
@ -2956,6 +2956,7 @@ type UserDevice struct {
|
|||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
Ip string `json:"ip"`
|
Ip string `json:"ip"`
|
||||||
Identifier string `json:"identifier"`
|
Identifier string `json:"identifier"`
|
||||||
|
DeviceNo string `json:"device_no"`
|
||||||
UserAgent string `json:"user_agent"`
|
UserAgent string `json:"user_agent"`
|
||||||
Online bool `json:"online"`
|
Online bool `json:"online"`
|
||||||
Enabled bool `json:"enabled"`
|
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.Println()
|
fmt.Println()
|
||||||
fmt.Print("[1/9] 连接源数据库... ")
|
fmt.Print("[1/10] 连接源数据库... ")
|
||||||
srcDB, err := gorm.Open(mysql.Open(*srcDSN), gormCfg)
|
srcDB, err := gorm.Open(mysql.Open(*srcDSN), gormCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("源库连接失败: %v", err)
|
log.Fatalf("源库连接失败: %v", err)
|
||||||
@ -261,7 +261,7 @@ func main() {
|
|||||||
fmt.Println("OK")
|
fmt.Println("OK")
|
||||||
|
|
||||||
// 连接目标库
|
// 连接目标库
|
||||||
fmt.Print("[2/9] 连接目标数据库... ")
|
fmt.Print("[2/10] 连接目标数据库... ")
|
||||||
dstDB, err := gorm.Open(mysql.Open(*dstDSN), gormCfg)
|
dstDB, err := gorm.Open(mysql.Open(*dstDSN), gormCfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("目标库连接失败: %v", err)
|
log.Fatalf("目标库连接失败: %v", err)
|
||||||
@ -269,7 +269,7 @@ func main() {
|
|||||||
fmt.Println("OK")
|
fmt.Println("OK")
|
||||||
|
|
||||||
// ── Step 3: 查询付费用户 ID ──
|
// ── Step 3: 查询付费用户 ID ──
|
||||||
fmt.Print("[3/9] 查询付费用户... ")
|
fmt.Print("[3/10] 查询付费用户... ")
|
||||||
var paidIDs []int64
|
var paidIDs []int64
|
||||||
err = srcDB.Raw(`
|
err = srcDB.Raw(`
|
||||||
SELECT DISTINCT t.uid FROM (
|
SELECT DISTINCT t.uid FROM (
|
||||||
@ -291,7 +291,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── Step 4: 读取源库数据 ──
|
// ── Step 4: 读取源库数据 ──
|
||||||
fmt.Print("[4/9] 读取源库数据... ")
|
fmt.Print("[4/10] 读取源库数据... ")
|
||||||
var (
|
var (
|
||||||
users []User
|
users []User
|
||||||
auths []AuthMethod
|
auths []AuthMethod
|
||||||
@ -374,6 +374,186 @@ func main() {
|
|||||||
fmt.Printf("\n 多设备拆分: %d 个设备 → 独立用户\n", len(splits))
|
fmt.Printf("\n 多设备拆分: %d 个设备 → 独立用户\n", len(splits))
|
||||||
fmt.Println()
|
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 {
|
if *dryRun {
|
||||||
fmt.Println("[DRY-RUN] 仅分析,不写入目标库")
|
fmt.Println("[DRY-RUN] 仅分析,不写入目标库")
|
||||||
return
|
return
|
||||||
@ -381,7 +561,7 @@ func main() {
|
|||||||
|
|
||||||
// ── Step 5: 清空目标库(可选) ──
|
// ── Step 5: 清空目标库(可选) ──
|
||||||
if *clean {
|
if *clean {
|
||||||
fmt.Print("[5/9] 清空目标库... ")
|
fmt.Print("[5/10] 清空目标库... ")
|
||||||
dstDB.Exec("SET FOREIGN_KEY_CHECKS = 0")
|
dstDB.Exec("SET FOREIGN_KEY_CHECKS = 0")
|
||||||
for _, tbl := range []string{
|
for _, tbl := range []string{
|
||||||
"user", "user_auth_methods", "user_device",
|
"user", "user_auth_methods", "user_device",
|
||||||
@ -393,11 +573,11 @@ func main() {
|
|||||||
dstDB.Exec("SET FOREIGN_KEY_CHECKS = 1")
|
dstDB.Exec("SET FOREIGN_KEY_CHECKS = 1")
|
||||||
fmt.Println("OK")
|
fmt.Println("OK")
|
||||||
} else {
|
} else {
|
||||||
fmt.Println("[5/9] 跳过清空(未指定 -clean)")
|
fmt.Println("[5/10] 跳过清空(未指定 -clean)")
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Step 6: 写入全量配置表 ──
|
// ── Step 6: 写入全量配置表 ──
|
||||||
fmt.Print("[6/9] 写入全量配置表... ")
|
fmt.Print("[6/10] 写入全量配置表... ")
|
||||||
if len(subPlans) > 0 {
|
if len(subPlans) > 0 {
|
||||||
dstDB.Exec("DELETE FROM subscribe") // 先清再插
|
dstDB.Exec("DELETE FROM subscribe") // 先清再插
|
||||||
for _, s := range subPlans {
|
for _, s := range subPlans {
|
||||||
@ -413,7 +593,7 @@ func main() {
|
|||||||
fmt.Println("OK")
|
fmt.Println("OK")
|
||||||
|
|
||||||
// ── Step 7: 写入付费用户数据(事务) ──
|
// ── Step 7: 写入付费用户数据(事务) ──
|
||||||
fmt.Print("[7/9] 写入付费用户数据... ")
|
fmt.Print("[7/10] 写入付费用户数据... ")
|
||||||
err = dstDB.Transaction(func(tx *gorm.DB) error {
|
err = dstDB.Transaction(func(tx *gorm.DB) error {
|
||||||
tx.Exec("SET FOREIGN_KEY_CHECKS = 0")
|
tx.Exec("SET FOREIGN_KEY_CHECKS = 0")
|
||||||
|
|
||||||
@ -445,7 +625,7 @@ func main() {
|
|||||||
fmt.Println("OK")
|
fmt.Println("OK")
|
||||||
|
|
||||||
// ── Step 8: 创建家庭组 ──
|
// ── Step 8: 创建家庭组 ──
|
||||||
fmt.Print("[8/9] 创建家庭组... ")
|
fmt.Print("[8/10] 创建家庭组... ")
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
familyCount := 0
|
familyCount := 0
|
||||||
|
|
||||||
@ -485,7 +665,7 @@ func main() {
|
|||||||
fmt.Printf("%d 个家庭组\n", familyCount)
|
fmt.Printf("%d 个家庭组\n", familyCount)
|
||||||
|
|
||||||
// ── Step 9: 多设备拆分 ──
|
// ── Step 9: 多设备拆分 ──
|
||||||
fmt.Print("[9/9] 多设备拆分... ")
|
fmt.Print("[9/10] 多设备拆分... ")
|
||||||
splitCount := 0
|
splitCount := 0
|
||||||
|
|
||||||
err = dstDB.Transaction(func(tx *gorm.DB) error {
|
err = dstDB.Transaction(func(tx *gorm.DB) error {
|
||||||
@ -553,6 +733,35 @@ func main() {
|
|||||||
}
|
}
|
||||||
fmt.Printf("%d 个设备\n", splitCount)
|
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()
|
||||||
fmt.Println("=== 迁移完成 ===")
|
fmt.Println("=== 迁移完成 ===")
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user