This commit is contained in:
parent
3cd22d8538
commit
dcdbabdb13
@ -101,11 +101,12 @@ type EmailConfig struct {
|
|||||||
EnableNotify bool `yaml:"enable_notify"`
|
EnableNotify bool `yaml:"enable_notify"`
|
||||||
EnableDomainSuffix bool `yaml:"enable_domain_suffix"`
|
EnableDomainSuffix bool `yaml:"enable_domain_suffix"`
|
||||||
DomainSuffixList string `yaml:"domain_suffix_list"`
|
DomainSuffixList string `yaml:"domain_suffix_list"`
|
||||||
VerifyEmailTemplate string `yaml:"verify_email_template"`
|
VerifyEmailTemplate string `yaml:"verify_email_template"`
|
||||||
VerifyEmailTemplates map[string]string `yaml:"verify_email_templates"`
|
VerifyEmailTemplates map[string]string `yaml:"verify_email_templates"`
|
||||||
ExpirationEmailTemplate string `yaml:"expiration_email_template"`
|
ExpirationEmailTemplate string `yaml:"expiration_email_template"`
|
||||||
MaintenanceEmailTemplate string `yaml:"maintenance_email_template"`
|
MaintenanceEmailTemplate string `yaml:"maintenance_email_template"`
|
||||||
TrafficExceedEmailTemplate string `yaml:"traffic_exceed_email_template"`
|
TrafficExceedEmailTemplate string `yaml:"traffic_exceed_email_template"`
|
||||||
|
DeleteAccountEmailTemplate string `yaml:"delete_account_email_template"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MobileConfig struct {
|
type MobileConfig struct {
|
||||||
|
|||||||
@ -82,7 +82,7 @@ func (l *GetFamilyDetailLogic) GetFamilyDetail(req *types.GetFamilyDetailRequest
|
|||||||
memberItem := types.FamilyMemberItem{
|
memberItem := types.FamilyMemberItem{
|
||||||
UserId: member.UserId,
|
UserId: member.UserId,
|
||||||
Identifier: identifier,
|
Identifier: identifier,
|
||||||
DeviceNo: deviceNoMap[member.UserId],
|
DeviceID: deviceNoMap[member.UserId],
|
||||||
Role: member.Role,
|
Role: member.Role,
|
||||||
RoleName: mapFamilyRoleName(member.Role),
|
RoleName: mapFamilyRoleName(member.Role),
|
||||||
Status: member.Status,
|
Status: member.Status,
|
||||||
|
|||||||
@ -37,7 +37,7 @@ func (l *GetUserDetailLogic) GetUserDetail(req *types.GetDetailRequest) (*types.
|
|||||||
tool.DeepCopy(&resp, userInfo)
|
tool.DeepCopy(&resp, userInfo)
|
||||||
for i, d := range userInfo.UserDevices {
|
for i, d := range userInfo.UserDevices {
|
||||||
if i < len(resp.UserDevices) {
|
if i < len(resp.UserDevices) {
|
||||||
resp.UserDevices[i].DeviceNo = tool.DeviceIdToHash(d.Id)
|
resp.UserDevices[i].DeviceID = tool.DeviceIdToHash(d.Id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if referCode := strings.TrimSpace(resp.ReferCode); referCode != "" {
|
if referCode := strings.TrimSpace(resp.ReferCode); referCode != "" {
|
||||||
|
|||||||
@ -163,10 +163,10 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge
|
|||||||
}
|
}
|
||||||
u.AuthMethods = authMethods
|
u.AuthMethods = authMethods
|
||||||
|
|
||||||
// 填充 DeviceNo
|
// 填充 DeviceID
|
||||||
for i, d := range item.UserDevices {
|
for i, d := range item.UserDevices {
|
||||||
if i < len(u.UserDevices) {
|
if i < len(u.UserDevices) {
|
||||||
u.UserDevices[i].DeviceNo = tool.DeviceIdToHash(d.Id)
|
u.UserDevices[i].DeviceID = tool.DeviceIdToHash(d.Id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -36,7 +36,7 @@ func (l *GetUserSubscribeDevicesLogic) GetUserSubscribeDevices(req *types.GetUse
|
|||||||
tool.DeepCopy(&userRespList, list)
|
tool.DeepCopy(&userRespList, list)
|
||||||
for i, d := range list {
|
for i, d := range list {
|
||||||
if i < len(userRespList) {
|
if i < len(userRespList) {
|
||||||
userRespList[i].DeviceNo = tool.DeviceIdToHash(d.Id)
|
userRespList[i].DeviceID = tool.DeviceIdToHash(d.Id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return &types.GetUserSubscribeDevicesResponse{
|
return &types.GetUserSubscribeDevicesResponse{
|
||||||
|
|||||||
@ -89,7 +89,11 @@ func (l *SendEmailCodeLogic) SendEmailCode(req *types.SendCodeRequest) (resp *ty
|
|||||||
// Generate verification code
|
// Generate verification code
|
||||||
code := random.Key(6, 0)
|
code := random.Key(6, 0)
|
||||||
scene := constant.ParseVerifyType(req.Type).String()
|
scene := constant.ParseVerifyType(req.Type).String()
|
||||||
taskPayload.Type = queue.EmailTypeVerify
|
if scene == constant.DeleteAccount.String() {
|
||||||
|
taskPayload.Type = queue.EmailTypeDeleteAccount
|
||||||
|
} else {
|
||||||
|
taskPayload.Type = queue.EmailTypeVerify
|
||||||
|
}
|
||||||
taskPayload.Scene = scene
|
taskPayload.Scene = scene
|
||||||
taskPayload.Email = req.Email
|
taskPayload.Email = req.Email
|
||||||
taskPayload.Subject = "Verification code"
|
taskPayload.Subject = "Verification code"
|
||||||
|
|||||||
@ -38,7 +38,7 @@ func (l *GetDeviceListLogic) GetDeviceList() (resp *types.GetDeviceListResponse,
|
|||||||
tool.DeepCopy(&userRespList, list)
|
tool.DeepCopy(&userRespList, list)
|
||||||
for i, d := range list {
|
for i, d := range list {
|
||||||
if i < len(userRespList) {
|
if i < len(userRespList) {
|
||||||
userRespList[i].DeviceNo = tool.DeviceIdToHash(d.Id)
|
userRespList[i].DeviceID = tool.DeviceIdToHash(d.Id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resp = &types.GetDeviceListResponse{
|
resp = &types.GetDeviceListResponse{
|
||||||
|
|||||||
@ -47,7 +47,7 @@ func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) {
|
|||||||
tool.DeepCopy(resp, u)
|
tool.DeepCopy(resp, u)
|
||||||
for i, d := range u.UserDevices {
|
for i, d := range u.UserDevices {
|
||||||
if i < len(resp.UserDevices) {
|
if i < len(resp.UserDevices) {
|
||||||
resp.UserDevices[i].DeviceNo = tool.DeviceIdToHash(d.Id)
|
resp.UserDevices[i].DeviceID = tool.DeviceIdToHash(d.Id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// refer_code 为空时自动生成
|
// refer_code 为空时自动生成
|
||||||
|
|||||||
@ -113,17 +113,18 @@ func (l *TelegramAuthConfig) Unmarshal(data string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type EmailAuthConfig struct {
|
type EmailAuthConfig struct {
|
||||||
Platform string `json:"platform"`
|
Platform string `json:"platform"`
|
||||||
PlatformConfig interface{} `json:"platform_config"`
|
PlatformConfig interface{} `json:"platform_config"`
|
||||||
EnableVerify bool `json:"enable_verify"`
|
EnableVerify bool `json:"enable_verify"`
|
||||||
EnableNotify bool `json:"enable_notify"`
|
EnableNotify bool `json:"enable_notify"`
|
||||||
EnableDomainSuffix bool `json:"enable_domain_suffix"`
|
EnableDomainSuffix bool `json:"enable_domain_suffix"`
|
||||||
DomainSuffixList string `json:"domain_suffix_list"`
|
DomainSuffixList string `json:"domain_suffix_list"`
|
||||||
VerifyEmailTemplate string `json:"verify_email_template"`
|
VerifyEmailTemplate string `json:"verify_email_template"`
|
||||||
VerifyEmailTemplates map[string]string `json:"verify_email_templates"`
|
VerifyEmailTemplates map[string]string `json:"verify_email_templates"`
|
||||||
ExpirationEmailTemplate string `json:"expiration_email_template"`
|
ExpirationEmailTemplate string `json:"expiration_email_template"`
|
||||||
MaintenanceEmailTemplate string `json:"maintenance_email_template"`
|
MaintenanceEmailTemplate string `json:"maintenance_email_template"`
|
||||||
TrafficExceedEmailTemplate string `json:"traffic_exceed_email_template"`
|
TrafficExceedEmailTemplate string `json:"traffic_exceed_email_template"`
|
||||||
|
DeleteAccountEmailTemplate string `json:"delete_account_email_template"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *EmailAuthConfig) Marshal() string {
|
func (l *EmailAuthConfig) Marshal() string {
|
||||||
@ -136,6 +137,9 @@ func (l *EmailAuthConfig) Marshal() string {
|
|||||||
if l.TrafficExceedEmailTemplate == "" {
|
if l.TrafficExceedEmailTemplate == "" {
|
||||||
l.TrafficExceedEmailTemplate = email.DefaultTrafficExceedEmailTemplate
|
l.TrafficExceedEmailTemplate = email.DefaultTrafficExceedEmailTemplate
|
||||||
}
|
}
|
||||||
|
if l.DeleteAccountEmailTemplate == "" {
|
||||||
|
l.DeleteAccountEmailTemplate = email.DefaultDeleteAccountEmailTemplate
|
||||||
|
}
|
||||||
if l.VerifyEmailTemplate == "" {
|
if l.VerifyEmailTemplate == "" {
|
||||||
l.VerifyEmailTemplate = email.DefaultEmailVerifyTemplate
|
l.VerifyEmailTemplate = email.DefaultEmailVerifyTemplate
|
||||||
}
|
}
|
||||||
@ -145,17 +149,18 @@ func (l *EmailAuthConfig) Marshal() string {
|
|||||||
bytes, err := json.Marshal(l)
|
bytes, err := json.Marshal(l)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
config := &EmailAuthConfig{
|
config := &EmailAuthConfig{
|
||||||
Platform: "smtp",
|
Platform: "smtp",
|
||||||
PlatformConfig: new(SMTPConfig),
|
PlatformConfig: new(SMTPConfig),
|
||||||
EnableVerify: true,
|
EnableVerify: true,
|
||||||
EnableNotify: true,
|
EnableNotify: true,
|
||||||
EnableDomainSuffix: false,
|
EnableDomainSuffix: false,
|
||||||
DomainSuffixList: "",
|
DomainSuffixList: "",
|
||||||
VerifyEmailTemplate: email.DefaultEmailVerifyTemplate,
|
VerifyEmailTemplate: email.DefaultEmailVerifyTemplate,
|
||||||
VerifyEmailTemplates: map[string]string{},
|
VerifyEmailTemplates: map[string]string{},
|
||||||
ExpirationEmailTemplate: email.DefaultExpirationEmailTemplate,
|
ExpirationEmailTemplate: email.DefaultExpirationEmailTemplate,
|
||||||
MaintenanceEmailTemplate: email.DefaultMaintenanceEmailTemplate,
|
MaintenanceEmailTemplate: email.DefaultMaintenanceEmailTemplate,
|
||||||
TrafficExceedEmailTemplate: email.DefaultTrafficExceedEmailTemplate,
|
TrafficExceedEmailTemplate: email.DefaultTrafficExceedEmailTemplate,
|
||||||
|
DeleteAccountEmailTemplate: email.DefaultDeleteAccountEmailTemplate,
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes, _ = json.Marshal(config)
|
bytes, _ = json.Marshal(config)
|
||||||
@ -189,6 +194,9 @@ func (l *EmailAuthConfig) Unmarshal(data string) {
|
|||||||
if l.VerifyEmailTemplates == nil {
|
if l.VerifyEmailTemplates == nil {
|
||||||
l.VerifyEmailTemplates = map[string]string{}
|
l.VerifyEmailTemplates = map[string]string{}
|
||||||
}
|
}
|
||||||
|
if l.DeleteAccountEmailTemplate == "" {
|
||||||
|
l.DeleteAccountEmailTemplate = email.DefaultDeleteAccountEmailTemplate
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SMTPConfig Email SMTP configuration
|
// SMTPConfig Email SMTP configuration
|
||||||
|
|||||||
@ -682,7 +682,7 @@ type FamilyDetail struct {
|
|||||||
type FamilyMemberItem struct {
|
type FamilyMemberItem struct {
|
||||||
UserId int64 `json:"user_id"`
|
UserId int64 `json:"user_id"`
|
||||||
Identifier string `json:"identifier"`
|
Identifier string `json:"identifier"`
|
||||||
DeviceNo string `json:"device_no"`
|
DeviceID string `json:"device_id"`
|
||||||
Role uint8 `json:"role"`
|
Role uint8 `json:"role"`
|
||||||
RoleName string `json:"role_name"`
|
RoleName string `json:"role_name"`
|
||||||
Status uint8 `json:"status"`
|
Status uint8 `json:"status"`
|
||||||
@ -2957,7 +2957,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"`
|
DeviceID string `json:"device_id"`
|
||||||
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"`
|
||||||
|
|||||||
@ -27,12 +27,12 @@ const (
|
|||||||
<tr>
|
<tr>
|
||||||
<td style="padding:0 43px 0 43px;">
|
<td style="padding:0 43px 0 43px;">
|
||||||
<h1 style="font-weight:600; font-size:36px; line-height:1.4; color:#0F2C53; margin:0 0 24px 0;">
|
<h1 style="font-weight:600; font-size:36px; line-height:1.4; color:#0F2C53; margin:0 0 24px 0;">
|
||||||
注销验证
|
验证码
|
||||||
</h1>
|
</h1>
|
||||||
<p style="font-weight:400; font-size:16px; line-height:1.4; color:#0F2C53; margin:0 0 24px 0; white-space:pre-line;">
|
<p style="font-weight:400; font-size:16px; line-height:1.4; color:#0F2C53; margin:0 0 24px 0; white-space:pre-line;">
|
||||||
亲爱的用户,
|
亲爱的用户,
|
||||||
|
|
||||||
我们收到了你在{{.SiteName}}的注销请求
|
你正在进行{{.SiteName}}账户相关操作
|
||||||
请在系统提示时输入以下验证码:
|
请在系统提示时输入以下验证码:
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -300,4 +300,42 @@ const (
|
|||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
|
|
||||||
|
DefaultDeleteAccountEmailTemplate = `<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<style>
|
||||||
|
body { margin: 0; padding: 0; background: #f4f4f4; font-family: 'PingFang SC', 'Microsoft YaHei', Arial, sans-serif; }
|
||||||
|
.container { max-width: 600px; margin: 0 auto; background: #fff; padding: 40px; }
|
||||||
|
.logo { text-align: right; margin-bottom: 40px; }
|
||||||
|
.title { font-size: 28px; font-weight: 600; color: #0F2C53; margin-bottom: 16px; }
|
||||||
|
.content { font-size: 16px; color: #0F2C53; line-height: 1.6; }
|
||||||
|
.warning-box { background: #FFF3CD; border-left: 4px solid #FF8C00; padding: 16px 20px; margin: 24px 0; border-radius: 4px; }
|
||||||
|
.warning-box p { margin: 0; font-size: 15px; color: #5C3D00; }
|
||||||
|
.footer { margin-top: 40px; font-size: 12px; color: #999; text-align: center; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<div class="logo">
|
||||||
|
{{if .SiteLogo}}<img src="{{.SiteLogo}}" alt="{{.SiteName}}" height="47" style="display:block;" />{{end}}
|
||||||
|
</div>
|
||||||
|
<div class="title">账户注销确认</div>
|
||||||
|
<div class="content">
|
||||||
|
<p>您好,</p>
|
||||||
|
<p>我们收到了您注销 <strong>{{.SiteName}}</strong> 账户的请求。请在系统提示时输入以下验证码以完成注销:</p>
|
||||||
|
<div style="font-size: 48px; font-weight: 700; color: #0F2C53; letter-spacing: 4px; margin: 24px 0;">{{.Code}}</div>
|
||||||
|
<p>验证码将在 <strong>{{.Expire}} 分钟</strong>后过期。</p>
|
||||||
|
<div class="warning-box">
|
||||||
|
<p>⚠️ <strong>重要提示:</strong>账户注销后,您的所有数据(包括订阅、套餐、账户信息)将被<strong>永久删除且不可恢复</strong>。请确认您已充分了解此操作的后果。</p>
|
||||||
|
</div>
|
||||||
|
<p>如果这不是您本人的操作,请立即忽略本邮件,您的账户将保持安全。</p>
|
||||||
|
<p>谢谢,<br />{{.SiteName}} 团队</p>
|
||||||
|
</div>
|
||||||
|
<div class="footer">此为系统邮件,请勿回复</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
)
|
)
|
||||||
|
|||||||
@ -60,14 +60,6 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro
|
|||||||
scene := resolveVerifyScene(payload.Scene, typeVal)
|
scene := resolveVerifyScene(payload.Scene, typeVal)
|
||||||
tplStr := selectVerifyTemplate(l.svcCtx.Config.Email.VerifyEmailTemplates, l.svcCtx.Config.Email.VerifyEmailTemplate, scene)
|
tplStr := selectVerifyTemplate(l.svcCtx.Config.Email.VerifyEmailTemplates, l.svcCtx.Config.Email.VerifyEmailTemplate, scene)
|
||||||
|
|
||||||
if tplStr == l.svcCtx.Config.Email.VerifyEmailTemplate &&
|
|
||||||
scene == constant.DeleteAccount.String() &&
|
|
||||||
!strings.Contains(tplStr, "Type 4") &&
|
|
||||||
!strings.Contains(tplStr, "Type eq 4") {
|
|
||||||
logger.WithContext(ctx).Infow("[SendEmailLogic] configured legacy verify template may not support DeleteAccount, fallback to default template")
|
|
||||||
tplStr = email.DefaultEmailVerifyTemplate
|
|
||||||
}
|
|
||||||
|
|
||||||
tpl, _ := template.New("verify").Parse(tplStr)
|
tpl, _ := template.New("verify").Parse(tplStr)
|
||||||
var result bytes.Buffer
|
var result bytes.Buffer
|
||||||
|
|
||||||
@ -80,6 +72,23 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
content = result.String()
|
content = result.String()
|
||||||
|
case types.EmailTypeDeleteAccount:
|
||||||
|
tplStr := l.svcCtx.Config.Email.DeleteAccountEmailTemplate
|
||||||
|
if tplStr == "" {
|
||||||
|
tplStr = email.DefaultDeleteAccountEmailTemplate
|
||||||
|
}
|
||||||
|
tpl, _ := template.New("delete_account").Parse(tplStr)
|
||||||
|
var result bytes.Buffer
|
||||||
|
err = tpl.Execute(&result, payload.Content)
|
||||||
|
if err != nil {
|
||||||
|
logger.WithContext(ctx).Error("[SendEmailLogic] Execute template failed",
|
||||||
|
logger.Field("error", err.Error()),
|
||||||
|
logger.Field("template", l.svcCtx.Config.Email.DeleteAccountEmailTemplate),
|
||||||
|
logger.Field("data", payload.Content),
|
||||||
|
)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
content = result.String()
|
||||||
case types.EmailTypeMaintenance:
|
case types.EmailTypeMaintenance:
|
||||||
tplStr := l.svcCtx.Config.Email.MaintenanceEmailTemplate
|
tplStr := l.svcCtx.Config.Email.MaintenanceEmailTemplate
|
||||||
if tplStr == "" {
|
if tplStr == "" {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ const (
|
|||||||
EmailTypeMaintenance = "maintenance"
|
EmailTypeMaintenance = "maintenance"
|
||||||
EmailTypeExpiration = "expiration"
|
EmailTypeExpiration = "expiration"
|
||||||
EmailTypeTrafficExceed = "traffic_exceed"
|
EmailTypeTrafficExceed = "traffic_exceed"
|
||||||
|
EmailTypeDeleteAccount = "delete_account"
|
||||||
EmailTypeCustom = "custom"
|
EmailTypeCustom = "custom"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -313,6 +313,51 @@ func main() {
|
|||||||
srcDB.Find(&subPlans)
|
srcDB.Find(&subPlans)
|
||||||
srcDB.Find(&payments)
|
srcDB.Find(&payments)
|
||||||
|
|
||||||
|
// ── 处理多订阅:如果用户有多个订阅,仅保留未过期的 ──
|
||||||
|
nowTime := time.Now()
|
||||||
|
subByUser := make(map[int64][]UserSubscribe)
|
||||||
|
for _, s := range subscribes {
|
||||||
|
subByUser[s.UserId] = append(subByUser[s.UserId], s)
|
||||||
|
}
|
||||||
|
|
||||||
|
var validSubscribes []UserSubscribe
|
||||||
|
for _, subs := range subByUser {
|
||||||
|
if len(subs) <= 1 {
|
||||||
|
// 单个订阅直接保留
|
||||||
|
validSubscribes = append(validSubscribes, subs...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var unexpired []UserSubscribe
|
||||||
|
var latest *UserSubscribe
|
||||||
|
|
||||||
|
for i := range subs {
|
||||||
|
s := subs[i]
|
||||||
|
// 如果没有过期时间,或者过期时间在当前时间之后
|
||||||
|
if s.ExpireTime == nil || s.ExpireTime.After(nowTime) {
|
||||||
|
unexpired = append(unexpired, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 记录到期时间最晚的一个,以防全部都过期了
|
||||||
|
if latest == nil {
|
||||||
|
latest = &s
|
||||||
|
} else if latest.ExpireTime != nil && s.ExpireTime != nil && s.ExpireTime.After(*latest.ExpireTime) {
|
||||||
|
latest = &s
|
||||||
|
} else if latest.ExpireTime != nil && s.ExpireTime == nil {
|
||||||
|
latest = &s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(unexpired) > 0 {
|
||||||
|
// 存在未过期的订阅,仅保留所有未过期的
|
||||||
|
validSubscribes = append(validSubscribes, unexpired...)
|
||||||
|
} else if latest != nil {
|
||||||
|
// 如果全部过期,仅保留到期时间最晚的那一个
|
||||||
|
validSubscribes = append(validSubscribes, *latest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subscribes = validSubscribes
|
||||||
|
|
||||||
fmt.Println("OK")
|
fmt.Println("OK")
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println(" 数据统计:")
|
fmt.Println(" 数据统计:")
|
||||||
@ -631,6 +676,11 @@ func main() {
|
|||||||
|
|
||||||
err = dstDB.Transaction(func(tx *gorm.DB) error {
|
err = dstDB.Transaction(func(tx *gorm.DB) error {
|
||||||
for _, uid := range paidIDs {
|
for _, uid := range paidIDs {
|
||||||
|
// 只为多设备用户创建家庭组
|
||||||
|
if len(deviceByUser[uid]) <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
family := UserFamily{
|
family := UserFamily{
|
||||||
OwnerUserId: uid,
|
OwnerUserId: uid,
|
||||||
MaxMembers: defaultFamilyMaxSize,
|
MaxMembers: defaultFamilyMaxSize,
|
||||||
@ -703,10 +753,40 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 查找原用户的家庭组
|
// 4. 查找原用户的家庭组(如果不存在则创建,虽然理论上 Step 8 已经为多设备用户创建了)
|
||||||
var family UserFamily
|
var family UserFamily
|
||||||
if err := tx.Where("owner_user_id = ?", s.OwnerUID).First(&family).Error; err != nil {
|
if err := tx.Where("owner_user_id = ?", s.OwnerUID).First(&family).Error; err != nil {
|
||||||
return fmt.Errorf("查找家庭组失败(owner=%d): %w", s.OwnerUID, err)
|
if err == gorm.ErrRecordNotFound {
|
||||||
|
// 补救措施:为该用户创建一个家庭组
|
||||||
|
family = UserFamily{
|
||||||
|
OwnerUserId: s.OwnerUID,
|
||||||
|
MaxMembers: defaultFamilyMaxSize,
|
||||||
|
Status: 1,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
if err := tx.Create(&family).Error; err != nil {
|
||||||
|
return fmt.Errorf("创建家庭组补救失败(owner=%d): %w", s.OwnerUID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建家主成员
|
||||||
|
ownerMember := UserFamilyMember{
|
||||||
|
FamilyId: family.Id,
|
||||||
|
UserId: s.OwnerUID,
|
||||||
|
Role: familyRoleOwner,
|
||||||
|
Status: 1,
|
||||||
|
JoinSource: "migration_split_recovery",
|
||||||
|
JoinedAt: now,
|
||||||
|
CreatedAt: now,
|
||||||
|
UpdatedAt: now,
|
||||||
|
}
|
||||||
|
if err := tx.Create(&ownerMember).Error; err != nil {
|
||||||
|
return fmt.Errorf("创建家主成员补救失败(owner=%d): %w", s.OwnerUID, err)
|
||||||
|
}
|
||||||
|
familyCount++ // 更新计数器
|
||||||
|
} else {
|
||||||
|
return fmt.Errorf("查找家庭组失败(owner=%d): %w", s.OwnerUID, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 加入家庭组
|
// 5. 加入家庭组
|
||||||
|
|||||||
7
说明文档.md
7
说明文档.md
@ -20,5 +20,8 @@ certbot certonly --manual --preferred-challenges dns -d airoport.win -d "*.airop
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
gunzip -c /Users/Apple/Downloads/db_backups_20260311_175705/mysql/mysql_dump_20260311_175556.sql.gz \
|
gunzip -c /Users/Apple/Downloads/db_backups_20260315_000003/mysql/mysql_dump_20260311_173933.sql.gz \
|
||||||
| docker exec -i ppanel-db mysql -uroot -prootpassword
|
| docker exec -i ppanel-db mysql -uroot -prootpassword
|
||||||
|
|
||||||
|
|
||||||
|
go run scripts/migrate_paid_users.go -src 'root:rootpassword@tcp(127.0.0.1:3306)/ppanel?charset=utf8mb4&parseTime=True&loc=Local' -dst 'root:jpcV41ppanel@tcp(154.12.35.103:3306)/ppanel?charset=utf8mb4&parseTime=True&loc=Local' -clean
|
||||||
Loading…
x
Reference in New Issue
Block a user