first commit
This commit is contained in:
@@ -0,0 +1,176 @@
|
||||
package mysqldao
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqlmodel"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// UserDao 用户数据访问
|
||||
type UserDao struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewUserDao 创建用户 DAO
|
||||
func NewUserDao() *UserDao {
|
||||
return &UserDao{db: global.MysqlEngine}
|
||||
}
|
||||
|
||||
// UserCreate 创建用户
|
||||
func (d *UserDao) UserCreate(ctx context.Context, user *mysqlmodel.User) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.UserCreate")
|
||||
defer span.End()
|
||||
|
||||
if err := d.db.WithContext(ctx).Create(user).Error; err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserUpdateByID 按主键 ID 更新用户字段
|
||||
func (d *UserDao) UserUpdateByID(ctx context.Context, userID int64, updates map[string]any) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.UserUpdateByID")
|
||||
defer span.End()
|
||||
|
||||
if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}).Where("id = ?", userID).Updates(updates).Error; err != nil {
|
||||
span.RecordError(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// VIPGrantIdempotent 幂等续费会员:事务内先原子累加 vip_expire(CASE 表达式避免并发丢失天数),
|
||||
// 回读真实到期时间后推导会员类型并写 VIP 流水;流水唯一索引(order_no+change_type)冲突时事务回滚,
|
||||
// 调用方据此识别为「已发放」从而幂等。
|
||||
//
|
||||
// 注意:续费 SQL 使用原生语句显式控制 SET 顺序(vip_start 先于 vip_expire 求值),
|
||||
// 使 vip_start 读到更新前的旧 vip_expire,保证「已过期续费时重置 vip_start」语义正确。
|
||||
//
|
||||
// 会员类型按「续费后最终到期时长」推导:>=365 天为年度会员,否则月度会员,
|
||||
// 避免「年度会员续月包被错误降级为月度」;体验会员等其它类型保持传入值不参与推导。
|
||||
func (d *UserDao) VIPGrantIdempotent(ctx context.Context, userID int64, now int64, memberDays int64, vipType int8, flow *mysqlmodel.VIPFlowLog) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.VIPGrantIdempotent")
|
||||
defer span.End()
|
||||
|
||||
return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
sql := `UPDATE users SET
|
||||
vip_start = CASE WHEN vip_type = 1 OR vip_expire <= ? THEN ? ELSE vip_start END,
|
||||
vip_expire = (CASE WHEN vip_expire > ? THEN vip_expire ELSE ? END) + ?
|
||||
WHERE id = ? AND deleted_at = 0`
|
||||
if res := tx.Exec(sql, now, now, now, now, memberDays*86400, userID); res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
|
||||
// 回读续费后的真实到期时间,用于推导会员类型。
|
||||
var renewed mysqlmodel.User
|
||||
if err := tx.Where("id = ?", userID).First(&renewed).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 字面量 2=月度会员 3=年度会员(与 service 层 VIPTypeMonthly/VIPTypeYearly 一致)。
|
||||
// 月度/年度续费按最终到期时长推导;体验会员(4)等其它类型保持传入值。
|
||||
finalType := vipType
|
||||
if vipType == 2 || vipType == 3 {
|
||||
if renewed.VIPExpire-now >= 365*86400 {
|
||||
finalType = 3
|
||||
} else {
|
||||
finalType = 2
|
||||
}
|
||||
}
|
||||
if err := tx.Model(&mysqlmodel.User{}).Where("id = ?", userID).
|
||||
UpdateColumn("vip_type", finalType).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
flow.NewVIPExpire = renewed.VIPExpire
|
||||
flow.NewVIPType = finalType
|
||||
|
||||
return tx.Create(flow).Error
|
||||
})
|
||||
}
|
||||
|
||||
// VIPRevokeIdempotent 幂等回退会员(退款回收):事务内先扣减 vip_expire(GREATEST 保证不早于 now),
|
||||
// 回读真实值后写回退流水;流水唯一索引(order_no+change_type)冲突时事务回滚,调用方据此识别为「已回退」。
|
||||
func (d *UserDao) VIPRevokeIdempotent(ctx context.Context, userID int64, now int64, memberDays int64, flow *mysqlmodel.VIPFlowLog) error {
|
||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.VIPRevokeIdempotent")
|
||||
defer span.End()
|
||||
|
||||
return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
// 1. 扣减 vip_expire(GREATEST(now, vip_expire - days) 保证不倒退到过去)。
|
||||
res := tx.Model(&mysqlmodel.User{}).
|
||||
Where("id = ? AND vip_expire > ?", userID, now).
|
||||
UpdateColumn("vip_expire", gorm.Expr("GREATEST(?, vip_expire - ?)", now, memberDays*86400))
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
|
||||
// 2. 回读真实值填充流水。
|
||||
var renewed mysqlmodel.User
|
||||
if err := tx.Where("id = ?", userID).First(&renewed).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
flow.NewVIPExpire = renewed.VIPExpire
|
||||
flow.NewVIPType = renewed.VIPType
|
||||
|
||||
// 3. 写回退流水(唯一索引冲突回滚整个事务,保证不重复回退)。
|
||||
return tx.Create(flow).Error
|
||||
})
|
||||
}
|
||||
|
||||
// UserGetByID 按主键 ID 查询用户
|
||||
func (d *UserDao) UserGetByID(ctx context.Context, userID int64) (*mysqlmodel.User, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.UserGetByID")
|
||||
defer span.End()
|
||||
|
||||
var user mysqlmodel.User
|
||||
if err := d.db.WithContext(ctx).First(&user, userID).Error; err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UserGetByIDs 按主键 ID 批量查询用户(空 ids 直接返回空切片)
|
||||
func (d *UserDao) UserGetByIDs(ctx context.Context, ids []int64) ([]mysqlmodel.User, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.UserGetByIDs")
|
||||
defer span.End()
|
||||
|
||||
var users []mysqlmodel.User
|
||||
if len(ids) == 0 {
|
||||
return users, nil
|
||||
}
|
||||
if err := d.db.WithContext(ctx).Where("id IN ?", ids).Find(&users).Error; err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// UserGetByUUID 按 UUID 查询用户
|
||||
func (d *UserDao) UserGetByUUID(ctx context.Context, userUUID string) (*mysqlmodel.User, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.UserGetByUUID")
|
||||
defer span.End()
|
||||
|
||||
var user mysqlmodel.User
|
||||
if err := d.db.WithContext(ctx).Where("user_uuid = ?", userUUID).First(&user).Error; err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UserGetByOpenID 按微信 openid 查询用户
|
||||
func (d *UserDao) UserGetByOpenID(ctx context.Context, openID string) (*mysqlmodel.User, error) {
|
||||
ctx, span := global.Tracer.Start(ctx, "mysqldao.UserGetByOpenID")
|
||||
defer span.End()
|
||||
|
||||
var user mysqlmodel.User
|
||||
if err := d.db.WithContext(ctx).Where("wx_open_id = ?", openID).First(&user).Error; err != nil {
|
||||
span.RecordError(err)
|
||||
return nil, err
|
||||
}
|
||||
return &user, nil
|
||||
}
|
||||
Reference in New Issue
Block a user