Compare commits

...

2 Commits

Author SHA1 Message Date
shanshanzhong147 c8258dc93b feat: 设备登录新增 base_payload 字段,前端传入后存储到 user_device 表
Build docker and publish / build (20.15.1) (push) Successful in 4m48s
2026-04-20 20:08:20 -07:00
shanshanzhong147 c0d839deb9 fix: 修复仪表盘时区统计偏移、重复订阅、新增map_apple字段
Build docker and publish / build (20.15.1) (push) Successful in 5m23s
- fix(order/model): QueryDateOrders/QueryDailyOrdersList 使用 time.Date 替代 Truncate 修复 UTC+8 时区偏移
- fix(user/model): QueryResisterUserTotalByDate 同样修复时区截断
- fix(traffic/model): QueryServerTrafficByDay 同样修复时区截断
- fix(activateOrder): 兜底查询防止过期用户重购产生重复订阅
- feat(api): SubscribeDiscount 新增 map_apple 字段
2026-04-20 02:34:23 -07:00
13 changed files with 97 additions and 36 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ env:
SSH_PASSWORD: ${{ github.ref_name == 'main' && vars.SSH_PASSWORD || vars.DEV_SSH_PASSWORD }}
# TG通知
TG_BOT_TOKEN: 8114337882:AAHkEx03HSu7RxN4IHBJJEnsK9aPPzNLIk0
TG_CHAT_ID: "-49402438031"
TG_CHAT_ID: "-4940243803"
# Go构建变量
SERVICE: vpn
SERVICE_STYLE: vpn
+6 -5
View File
@@ -149,11 +149,12 @@ type (
State string `form:"state"`
}
DeviceLoginRequest {
Identifier string `json:"identifier" validate:"required"`
IP string `header:"X-Original-Forwarded-For"`
UserAgent string `json:"user_agent" validate:"required"`
CfToken string `json:"cf_token,optional"`
ShortCode string `json:"short_code,optional"`
Identifier string `json:"identifier" validate:"required"`
IP string `header:"X-Original-Forwarded-For"`
UserAgent string `json:"user_agent" validate:"required"`
CfToken string `json:"cf_token,optional"`
ShortCode string `json:"short_code,optional"`
BasePayload string `json:"base_payload,optional"`
}
GenerateCaptchaResponse {
Id string `json:"id"`
+1
View File
@@ -227,6 +227,7 @@ type (
SubscribeDiscount {
Quantity int64 `json:"quantity"`
Discount float64 `json:"discount"`
MapApple string `json:"map_apple"`
}
TrafficLimit {
StatType string `json:"stat_type"`
@@ -0,0 +1 @@
ALTER TABLE `user_device` DROP COLUMN `base_payload`;
@@ -0,0 +1 @@
ALTER TABLE `user_device` ADD COLUMN `base_payload` TEXT DEFAULT NULL COMMENT 'Base Payload' AFTER `short_code`;
+19 -7
View File
@@ -96,6 +96,17 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user failed: %v", err.Error())
}
// Update base_payload if provided
if req.BasePayload != "" && req.BasePayload != deviceInfo.BasePayload {
deviceInfo.BasePayload = req.BasePayload
if updateErr := l.svcCtx.UserModel.UpdateDevice(l.ctx, deviceInfo); updateErr != nil {
l.Errorw("update device base_payload failed",
logger.Field("device_id", deviceInfo.Id),
logger.Field("error", updateErr.Error()),
)
}
}
// 注销后 device auth_method 被删除,重新登录时需要补回
hasDeviceAuth := false
for _, am := range userInfo.AuthMethods {
@@ -220,13 +231,14 @@ func (l *DeviceLoginLogic) registerUserAndDevice(req *types.DeviceLoginRequest)
// Insert device record
deviceInfo := &user.Device{
Ip: req.IP,
UserId: userInfo.Id,
UserAgent: req.UserAgent,
Identifier: req.Identifier,
ShortCode: req.ShortCode,
Enabled: true,
Online: false,
Ip: req.IP,
UserId: userInfo.Id,
UserAgent: req.UserAgent,
Identifier: req.Identifier,
ShortCode: req.ShortCode,
BasePayload: req.BasePayload,
Enabled: true,
Online: false,
}
if err := db.Create(deviceInfo).Error; err != nil {
l.Errorw("failed to insert device",
+3 -3
View File
@@ -166,8 +166,8 @@ func (m *customOrderModel) QueryMonthlyOrders(ctx context.Context, date time.Tim
// QueryDateOrders Query orders by date
func (m *customOrderModel) QueryDateOrders(ctx context.Context, date time.Time) (OrdersTotal, error) {
start := date.Truncate(24 * time.Hour)
end := start.Add(24 * time.Hour).Add(-time.Nanosecond)
start := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
end := start.AddDate(0, 0, 1).Add(-time.Nanosecond)
var result OrdersTotal
err := m.QueryNoCacheCtx(ctx, &result, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&Order{}).
@@ -276,7 +276,7 @@ func (m *customOrderModel) QueryDailyOrdersList(ctx context.Context, date time.T
// 当月 1 号 00:00:00
firstDay := time.Date(date.Year(), date.Month(), 1, 0, 0, 0, 0, date.Location())
// 第二天 00:00:00
nextDay := date.AddDate(0, 0, 1).Truncate(24 * time.Hour)
nextDay := time.Date(date.Year(), date.Month(), date.Day()+1, 0, 0, 0, 0, date.Location())
return conn.Model(&Order{}).
Select(`
+2 -2
View File
@@ -27,8 +27,8 @@ func NewModel(conn *gorm.DB) Model {
func (m *customTrafficModel) QueryServerTrafficByDay(ctx context.Context, serverId int64, date time.Time) (*TotalTraffic, error) {
var data TotalTraffic
start := date.Truncate(24 * time.Hour)
end := start.Add(24 * time.Hour).Add(-time.Nanosecond)
start := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
end := start.AddDate(0, 0, 1).Add(-time.Nanosecond)
err := m.Conn.WithContext(ctx).Model(&TrafficLog{}).
Select("sum(download) as download, sum(upload) as upload").
Where("server_id = ? AND timestamp BETWEEN ? AND ?", serverId, start, end).
+2 -2
View File
@@ -314,8 +314,8 @@ func (m *customUserModel) UpdateUserSubscribeWithTraffic(ctx context.Context, id
func (m *customUserModel) QueryResisterUserTotalByDate(ctx context.Context, date time.Time) (int64, error) {
var total int64
start := date.Truncate(24 * time.Hour)
end := start.Add(24 * time.Hour).Add(-time.Second)
start := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
end := start.AddDate(0, 0, 1).Add(-time.Second)
err := m.QueryNoCacheCtx(ctx, &total, func(conn *gorm.DB, v interface{}) error {
return conn.Model(&User{}).Where("created_at > ? and created_at < ?", start, end).Count(&total).Error
})
+11 -10
View File
@@ -130,16 +130,17 @@ func (*AuthMethods) TableName() string {
}
type Device struct {
Id int64 `gorm:"primaryKey"`
Ip string `gorm:"type:varchar(255);not null;comment:Device IP"`
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
UserAgent string `gorm:"default:null;comment:UserAgent."`
Identifier string `gorm:"type:varchar(255);unique;index:idx_identifier;default:'';comment:Device Identifier"`
ShortCode string `gorm:"type:varchar(255);default:'';comment:Short Code"`
Online bool `gorm:"default:false;not null;comment:Online"`
Enabled bool `gorm:"default:true;not null;comment:Enabled"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
Id int64 `gorm:"primaryKey"`
Ip string `gorm:"type:varchar(255);not null;comment:Device IP"`
UserId int64 `gorm:"index:idx_user_id;not null;comment:User ID"`
UserAgent string `gorm:"default:null;comment:UserAgent."`
Identifier string `gorm:"type:varchar(255);unique;index:idx_identifier;default:'';comment:Device Identifier"`
ShortCode string `gorm:"type:varchar(255);default:'';comment:Short Code"`
BasePayload string `gorm:"type:text;default:null;comment:Base Payload"`
Online bool `gorm:"default:false;not null;comment:Online"`
Enabled bool `gorm:"default:true;not null;comment:Enabled"`
CreatedAt time.Time `gorm:"<-:create;comment:Creation Time"`
UpdatedAt time.Time `gorm:"comment:Update Time"`
}
func (*Device) TableName() string {
+7 -5
View File
@@ -637,11 +637,12 @@ type DeviceAuthticateConfig struct {
}
type DeviceLoginRequest struct {
Identifier string `json:"identifier" validate:"required"`
IP string `header:"X-Original-Forwarded-For"`
UserAgent string `json:"user_agent" validate:"required"`
CfToken string `json:"cf_token,optional"`
ShortCode string `json:"short_code,optional"`
Identifier string `json:"identifier" validate:"required"`
IP string `header:"X-Original-Forwarded-For"`
UserAgent string `json:"user_agent" validate:"required"`
CfToken string `json:"cf_token,optional"`
ShortCode string `json:"short_code,optional"`
BasePayload string `json:"base_payload,optional"`
}
type DissolveFamilyRequest struct {
@@ -2655,6 +2656,7 @@ type SubscribeDiscount struct {
Quantity int64 `json:"quantity"`
Discount float64 `json:"discount"`
NewUserOnly bool `json:"new_user_only"`
MapApple string `json:"map_apple"`
}
type SubscribeGroup struct {
+42 -1
View File
@@ -292,7 +292,48 @@ func (l *ActivateOrderLogic) NewPurchase(ctx context.Context, orderInfo *order.O
}
}
// 如果没有合并赠送订阅,则正常创建新订阅
// 兜底:创建新订阅前,查找用户是否已有同套餐的订阅记录(含过期/赠送),
// 有则复用旧记录续期,避免出现重复订阅。
// 需要同时检查 UserId 和 SubscriptionUserId,因为家庭组绑定前后 owner 可能不同。
if userSub == nil {
candidateUserIds := []int64{orderInfo.UserId}
if orderInfo.SubscriptionUserId > 0 && orderInfo.SubscriptionUserId != orderInfo.UserId {
candidateUserIds = append(candidateUserIds, orderInfo.SubscriptionUserId)
}
var existingSub user.Subscribe
if findErr := l.svc.DB.Model(&user.Subscribe{}).
Where("user_id IN ? AND subscribe_id = ?", candidateUserIds, orderInfo.SubscribeId).
Order("expire_time DESC").
First(&existingSub).Error; findErr == nil {
// 家庭组场景:订阅 owner 可能变更(如成员注册的试用 → 被家主收归),
// 续期前把 user_id 校正为当前订单的 SubscriptionUserId
effectiveOwner := orderInfo.UserId
if orderInfo.SubscriptionUserId > 0 {
effectiveOwner = orderInfo.SubscriptionUserId
}
if existingSub.UserId != effectiveOwner {
existingSub.UserId = effectiveOwner
}
// 找到已有记录,走续期逻辑
if renewErr := l.updateSubscriptionForRenewal(ctx, &existingSub, sub, orderInfo); renewErr != nil {
logger.WithContext(ctx).Error("Fallback renew existing subscription failed, will create new",
logger.Field("error", renewErr.Error()),
logger.Field("existing_subscribe_id", existingSub.Id),
logger.Field("order_no", orderInfo.OrderNo),
)
} else {
userSub = &existingSub
logger.WithContext(ctx).Infow("Fallback: renewed existing subscription instead of creating duplicate",
logger.Field("existing_subscribe_id", existingSub.Id),
logger.Field("order_no", orderInfo.OrderNo),
logger.Field("candidate_user_ids", candidateUserIds),
logger.Field("owner_corrected_to", effectiveOwner),
)
}
}
}
// 如果仍然没有可复用的订阅,才创建新订阅
if userSub == nil {
userSub, err = l.createUserSubscription(ctx, orderInfo, sub)
if err != nil {
+1
View File
@@ -16,6 +16,7 @@
| 2026-03-12 | 分析并确认 Unknown column 错误 | [x] 已完成 | 确认为 `user_device` 缺少 `short_code` 字段,已提供 SQL |
| 2026-03-12 | 提供 SSL 证书替换指令 | [x] 已完成 | 已提供备份与替换证书的组合指令 |
| 2026-03-17 | 合并 internal 到 internal/main | [x] 已完成 | 已查验均为fast-forward,受限网络/权限,需手动push完成合并 |
| 2026-04-14 | 排查支付成功但订阅未下发问题 | [x] 已完成 | 已提供 Docker 相关的日志排查与数据库核对命令 |
certbot certonly --manual --preferred-challenges dns -d airoport.win -d "*.airoport.win" -d hifastapp.com