This commit is contained in:
parent
3cd22d8538
commit
dcdbabdb13
@ -101,11 +101,12 @@ type EmailConfig struct {
|
||||
EnableNotify bool `yaml:"enable_notify"`
|
||||
EnableDomainSuffix bool `yaml:"enable_domain_suffix"`
|
||||
DomainSuffixList string `yaml:"domain_suffix_list"`
|
||||
VerifyEmailTemplate string `yaml:"verify_email_template"`
|
||||
VerifyEmailTemplates map[string]string `yaml:"verify_email_templates"`
|
||||
ExpirationEmailTemplate string `yaml:"expiration_email_template"`
|
||||
MaintenanceEmailTemplate string `yaml:"maintenance_email_template"`
|
||||
TrafficExceedEmailTemplate string `yaml:"traffic_exceed_email_template"`
|
||||
VerifyEmailTemplate string `yaml:"verify_email_template"`
|
||||
VerifyEmailTemplates map[string]string `yaml:"verify_email_templates"`
|
||||
ExpirationEmailTemplate string `yaml:"expiration_email_template"`
|
||||
MaintenanceEmailTemplate string `yaml:"maintenance_email_template"`
|
||||
TrafficExceedEmailTemplate string `yaml:"traffic_exceed_email_template"`
|
||||
DeleteAccountEmailTemplate string `yaml:"delete_account_email_template"`
|
||||
}
|
||||
|
||||
type MobileConfig struct {
|
||||
|
||||
@ -82,7 +82,7 @@ func (l *GetFamilyDetailLogic) GetFamilyDetail(req *types.GetFamilyDetailRequest
|
||||
memberItem := types.FamilyMemberItem{
|
||||
UserId: member.UserId,
|
||||
Identifier: identifier,
|
||||
DeviceNo: deviceNoMap[member.UserId],
|
||||
DeviceID: deviceNoMap[member.UserId],
|
||||
Role: member.Role,
|
||||
RoleName: mapFamilyRoleName(member.Role),
|
||||
Status: member.Status,
|
||||
|
||||
@ -37,7 +37,7 @@ func (l *GetUserDetailLogic) GetUserDetail(req *types.GetDetailRequest) (*types.
|
||||
tool.DeepCopy(&resp, userInfo)
|
||||
for i, d := range userInfo.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 != "" {
|
||||
|
||||
@ -163,10 +163,10 @@ func (l *GetUserListLogic) GetUserList(req *types.GetUserListRequest) (*types.Ge
|
||||
}
|
||||
u.AuthMethods = authMethods
|
||||
|
||||
// 填充 DeviceNo
|
||||
// 填充 DeviceID
|
||||
for i, d := range item.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)
|
||||
for i, d := range list {
|
||||
if i < len(userRespList) {
|
||||
userRespList[i].DeviceNo = tool.DeviceIdToHash(d.Id)
|
||||
userRespList[i].DeviceID = tool.DeviceIdToHash(d.Id)
|
||||
}
|
||||
}
|
||||
return &types.GetUserSubscribeDevicesResponse{
|
||||
|
||||
@ -89,7 +89,11 @@ func (l *SendEmailCodeLogic) SendEmailCode(req *types.SendCodeRequest) (resp *ty
|
||||
// Generate verification code
|
||||
code := random.Key(6, 0)
|
||||
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.Email = req.Email
|
||||
taskPayload.Subject = "Verification code"
|
||||
|
||||
@ -38,7 +38,7 @@ func (l *GetDeviceListLogic) GetDeviceList() (resp *types.GetDeviceListResponse,
|
||||
tool.DeepCopy(&userRespList, list)
|
||||
for i, d := range list {
|
||||
if i < len(userRespList) {
|
||||
userRespList[i].DeviceNo = tool.DeviceIdToHash(d.Id)
|
||||
userRespList[i].DeviceID = tool.DeviceIdToHash(d.Id)
|
||||
}
|
||||
}
|
||||
resp = &types.GetDeviceListResponse{
|
||||
|
||||
@ -47,7 +47,7 @@ func (l *QueryUserInfoLogic) QueryUserInfo() (resp *types.User, err error) {
|
||||
tool.DeepCopy(resp, u)
|
||||
for i, d := range u.UserDevices {
|
||||
if i < len(resp.UserDevices) {
|
||||
resp.UserDevices[i].DeviceNo = tool.DeviceIdToHash(d.Id)
|
||||
resp.UserDevices[i].DeviceID = tool.DeviceIdToHash(d.Id)
|
||||
}
|
||||
}
|
||||
// refer_code 为空时自动生成
|
||||
|
||||
@ -113,17 +113,18 @@ func (l *TelegramAuthConfig) Unmarshal(data string) error {
|
||||
}
|
||||
|
||||
type EmailAuthConfig struct {
|
||||
Platform string `json:"platform"`
|
||||
PlatformConfig interface{} `json:"platform_config"`
|
||||
EnableVerify bool `json:"enable_verify"`
|
||||
EnableNotify bool `json:"enable_notify"`
|
||||
EnableDomainSuffix bool `json:"enable_domain_suffix"`
|
||||
DomainSuffixList string `json:"domain_suffix_list"`
|
||||
VerifyEmailTemplate string `json:"verify_email_template"`
|
||||
VerifyEmailTemplates map[string]string `json:"verify_email_templates"`
|
||||
ExpirationEmailTemplate string `json:"expiration_email_template"`
|
||||
MaintenanceEmailTemplate string `json:"maintenance_email_template"`
|
||||
TrafficExceedEmailTemplate string `json:"traffic_exceed_email_template"`
|
||||
Platform string `json:"platform"`
|
||||
PlatformConfig interface{} `json:"platform_config"`
|
||||
EnableVerify bool `json:"enable_verify"`
|
||||
EnableNotify bool `json:"enable_notify"`
|
||||
EnableDomainSuffix bool `json:"enable_domain_suffix"`
|
||||
DomainSuffixList string `json:"domain_suffix_list"`
|
||||
VerifyEmailTemplate string `json:"verify_email_template"`
|
||||
VerifyEmailTemplates map[string]string `json:"verify_email_templates"`
|
||||
ExpirationEmailTemplate string `json:"expiration_email_template"`
|
||||
MaintenanceEmailTemplate string `json:"maintenance_email_template"`
|
||||
TrafficExceedEmailTemplate string `json:"traffic_exceed_email_template"`
|
||||
DeleteAccountEmailTemplate string `json:"delete_account_email_template"`
|
||||
}
|
||||
|
||||
func (l *EmailAuthConfig) Marshal() string {
|
||||
@ -136,6 +137,9 @@ func (l *EmailAuthConfig) Marshal() string {
|
||||
if l.TrafficExceedEmailTemplate == "" {
|
||||
l.TrafficExceedEmailTemplate = email.DefaultTrafficExceedEmailTemplate
|
||||
}
|
||||
if l.DeleteAccountEmailTemplate == "" {
|
||||
l.DeleteAccountEmailTemplate = email.DefaultDeleteAccountEmailTemplate
|
||||
}
|
||||
if l.VerifyEmailTemplate == "" {
|
||||
l.VerifyEmailTemplate = email.DefaultEmailVerifyTemplate
|
||||
}
|
||||
@ -145,17 +149,18 @@ func (l *EmailAuthConfig) Marshal() string {
|
||||
bytes, err := json.Marshal(l)
|
||||
if err != nil {
|
||||
config := &EmailAuthConfig{
|
||||
Platform: "smtp",
|
||||
PlatformConfig: new(SMTPConfig),
|
||||
EnableVerify: true,
|
||||
EnableNotify: true,
|
||||
EnableDomainSuffix: false,
|
||||
DomainSuffixList: "",
|
||||
VerifyEmailTemplate: email.DefaultEmailVerifyTemplate,
|
||||
VerifyEmailTemplates: map[string]string{},
|
||||
ExpirationEmailTemplate: email.DefaultExpirationEmailTemplate,
|
||||
MaintenanceEmailTemplate: email.DefaultMaintenanceEmailTemplate,
|
||||
TrafficExceedEmailTemplate: email.DefaultTrafficExceedEmailTemplate,
|
||||
Platform: "smtp",
|
||||
PlatformConfig: new(SMTPConfig),
|
||||
EnableVerify: true,
|
||||
EnableNotify: true,
|
||||
EnableDomainSuffix: false,
|
||||
DomainSuffixList: "",
|
||||
VerifyEmailTemplate: email.DefaultEmailVerifyTemplate,
|
||||
VerifyEmailTemplates: map[string]string{},
|
||||
ExpirationEmailTemplate: email.DefaultExpirationEmailTemplate,
|
||||
MaintenanceEmailTemplate: email.DefaultMaintenanceEmailTemplate,
|
||||
TrafficExceedEmailTemplate: email.DefaultTrafficExceedEmailTemplate,
|
||||
DeleteAccountEmailTemplate: email.DefaultDeleteAccountEmailTemplate,
|
||||
}
|
||||
|
||||
bytes, _ = json.Marshal(config)
|
||||
@ -189,6 +194,9 @@ func (l *EmailAuthConfig) Unmarshal(data string) {
|
||||
if l.VerifyEmailTemplates == nil {
|
||||
l.VerifyEmailTemplates = map[string]string{}
|
||||
}
|
||||
if l.DeleteAccountEmailTemplate == "" {
|
||||
l.DeleteAccountEmailTemplate = email.DefaultDeleteAccountEmailTemplate
|
||||
}
|
||||
}
|
||||
|
||||
// SMTPConfig Email SMTP configuration
|
||||
|
||||
@ -682,7 +682,7 @@ type FamilyDetail struct {
|
||||
type FamilyMemberItem struct {
|
||||
UserId int64 `json:"user_id"`
|
||||
Identifier string `json:"identifier"`
|
||||
DeviceNo string `json:"device_no"`
|
||||
DeviceID string `json:"device_id"`
|
||||
Role uint8 `json:"role"`
|
||||
RoleName string `json:"role_name"`
|
||||
Status uint8 `json:"status"`
|
||||
@ -2957,7 +2957,7 @@ type UserDevice struct {
|
||||
Id int64 `json:"id"`
|
||||
Ip string `json:"ip"`
|
||||
Identifier string `json:"identifier"`
|
||||
DeviceNo string `json:"device_no"`
|
||||
DeviceID string `json:"device_id"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
Online bool `json:"online"`
|
||||
Enabled bool `json:"enabled"`
|
||||
|
||||
@ -27,12 +27,12 @@ const (
|
||||
<tr>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
@ -300,4 +300,42 @@ const (
|
||||
</div>
|
||||
</body>
|
||||
</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)
|
||||
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)
|
||||
var result bytes.Buffer
|
||||
|
||||
@ -80,6 +72,23 @@ func (l *SendEmailLogic) ProcessTask(ctx context.Context, task *asynq.Task) erro
|
||||
return nil
|
||||
}
|
||||
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:
|
||||
tplStr := l.svcCtx.Config.Email.MaintenanceEmailTemplate
|
||||
if tplStr == "" {
|
||||
|
||||
@ -10,6 +10,7 @@ const (
|
||||
EmailTypeMaintenance = "maintenance"
|
||||
EmailTypeExpiration = "expiration"
|
||||
EmailTypeTrafficExceed = "traffic_exceed"
|
||||
EmailTypeDeleteAccount = "delete_account"
|
||||
EmailTypeCustom = "custom"
|
||||
)
|
||||
|
||||
|
||||
@ -313,6 +313,51 @@ func main() {
|
||||
srcDB.Find(&subPlans)
|
||||
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()
|
||||
fmt.Println(" 数据统计:")
|
||||
@ -631,6 +676,11 @@ func main() {
|
||||
|
||||
err = dstDB.Transaction(func(tx *gorm.DB) error {
|
||||
for _, uid := range paidIDs {
|
||||
// 只为多设备用户创建家庭组
|
||||
if len(deviceByUser[uid]) <= 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
family := UserFamily{
|
||||
OwnerUserId: uid,
|
||||
MaxMembers: defaultFamilyMaxSize,
|
||||
@ -703,10 +753,40 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 查找原用户的家庭组
|
||||
// 4. 查找原用户的家庭组(如果不存在则创建,虽然理论上 Step 8 已经为多设备用户创建了)
|
||||
var family UserFamily
|
||||
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. 加入家庭组
|
||||
|
||||
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 \
|
||||
| docker exec -i ppanel-db mysql -uroot -prootpassword
|
||||
gunzip -c /Users/Apple/Downloads/db_backups_20260315_000003/mysql/mysql_dump_20260311_173933.sql.gz \
|
||||
| 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