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 }