197 lines
6.9 KiB
Go
197 lines
6.9 KiB
Go
package mysqldao
|
||
|
||
import (
|
||
"context"
|
||
|
||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/global"
|
||
"git.lzcwy.cn/zhengchiliu/zt-closet-backend/internal/mysqlmodel"
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
)
|
||
|
||
// 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 {
|
||
var before mysqlmodel.User
|
||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).Where("id = ?", userID).First(&before).Error; err != nil {
|
||
return err
|
||
}
|
||
// 购买开通/续费使用稳定的订单级幂等判断,不让重试后的 change_type 变化绕过唯一键。
|
||
if flow.ChangeType == 1 || flow.ChangeType == 2 {
|
||
var count int64
|
||
if err := tx.Model(&mysqlmodel.VIPFlowLog{}).Where("order_no = ? AND change_type IN ?", flow.OrderNo, []int8{1, 2}).Count(&count).Error; err != nil {
|
||
return err
|
||
}
|
||
if count > 0 {
|
||
return nil
|
||
}
|
||
flow.ChangeType = 1
|
||
if before.VIPType != 1 && before.VIPExpire > now {
|
||
flow.ChangeType = 2
|
||
}
|
||
flow.OldVIPType, flow.OldVIPExpire = before.VIPType, before.VIPExpire
|
||
}
|
||
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
|
||
}
|