first commit

This commit is contained in:
2026-09-11 01:41:52 +08:00
commit 9f22e6ccae
263 changed files with 28935 additions and 0 deletions
+176
View File
@@ -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
}