first commit

This commit is contained in:
2026-09-11 01:47:20 +08:00
commit 303207d3cc
74 changed files with 8348 additions and 0 deletions
+103
View File
@@ -0,0 +1,103 @@
package mysqldao
import (
"context"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqlmodel"
"gorm.io/gorm"
)
// AdminDao 管理员账号数据访问
type AdminDao struct {
db *gorm.DB
}
// NewAdminDao 创建管理员 DAO
func NewAdminDao() *AdminDao {
return &AdminDao{db: global.MysqlEngine}
}
// AdminGetByID 按主键 ID 查询管理员
func (d *AdminDao) AdminGetByID(ctx context.Context, id int64) (*mysqlmodel.AdminUser, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.AdminGetByID")
defer span.End()
var admin mysqlmodel.AdminUser
if err := d.db.WithContext(ctx).First(&admin, id).Error; err != nil {
span.RecordError(err)
return nil, err
}
return &admin, nil
}
// AdminGetByUsername 按登录账号查询管理员(含软删除)
func (d *AdminDao) AdminGetByUsername(ctx context.Context, username string) (*mysqlmodel.AdminUser, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.AdminGetByUsername")
defer span.End()
var admin mysqlmodel.AdminUser
if err := d.db.WithContext(ctx).Where("username = ?", username).First(&admin).Error; err != nil {
span.RecordError(err)
return nil, err
}
return &admin, nil
}
// AdminList 分页查询管理员列表(status 为 0 不筛选)
func (d *AdminDao) AdminList(ctx context.Context, status int8, page, pageSize int) ([]mysqlmodel.AdminUser, int64, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.AdminList")
defer span.End()
var list []mysqlmodel.AdminUser
var total int64
q := d.db.WithContext(ctx).Model(&mysqlmodel.AdminUser{})
if status != 0 {
q = q.Where("status = ?", status)
}
if err := q.Count(&total).Error; err != nil {
span.RecordError(err)
return nil, 0, err
}
if err := q.Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&list).Error; err != nil {
span.RecordError(err)
return nil, 0, err
}
return list, total, nil
}
// AdminCreate 创建管理员账号
func (d *AdminDao) AdminCreate(ctx context.Context, admin *mysqlmodel.AdminUser) error {
ctx, span := global.Tracer.Start(ctx, "mysqldao.AdminCreate")
defer span.End()
if err := d.db.WithContext(ctx).Create(admin).Error; err != nil {
span.RecordError(err)
return err
}
return nil
}
// AdminUpdate 按主键更新管理员字段
func (d *AdminDao) AdminUpdate(ctx context.Context, id int64, updates map[string]any) error {
ctx, span := global.Tracer.Start(ctx, "mysqldao.AdminUpdate")
defer span.End()
if err := d.db.WithContext(ctx).Model(&mysqlmodel.AdminUser{}).Where("id = ?", id).Updates(updates).Error; err != nil {
span.RecordError(err)
return err
}
return nil
}
// AdminSoftDelete 软删除管理员账号
func (d *AdminDao) AdminSoftDelete(ctx context.Context, id int64) error {
ctx, span := global.Tracer.Start(ctx, "mysqldao.AdminSoftDelete")
defer span.End()
if err := d.db.WithContext(ctx).Where("id = ?", id).Delete(&mysqlmodel.AdminUser{}).Error; err != nil {
span.RecordError(err)
return err
}
return nil
}
+79
View File
@@ -0,0 +1,79 @@
package mysqldao
import (
"context"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqlmodel"
"gorm.io/gorm"
)
// ClosetShareDao 衣柜分享数据访问
type ClosetShareDao struct {
db *gorm.DB
}
// NewClosetShareDao 创建衣柜分享 DAO
func NewClosetShareDao() *ClosetShareDao {
return &ClosetShareDao{db: global.MysqlEngine}
}
// ClosetShareListParams 分享分页列表筛选参数。
type ClosetShareListParams struct {
UserID int64 // 用户 ID,0 表示全部
Status int8 // 分享状态,0 表示全部
Page int
Size int
}
// ClosetShareList 分页查询衣柜分享
func (d *ClosetShareDao) ClosetShareList(ctx context.Context, p ClosetShareListParams) ([]mysqlmodel.ClosetShare, int64, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.ClosetShareList")
defer span.End()
var list []mysqlmodel.ClosetShare
var total int64
q := d.db.WithContext(ctx).Model(&mysqlmodel.ClosetShare{})
if p.UserID != 0 {
q = q.Where("user_id = ?", p.UserID)
}
if p.Status != 0 {
q = q.Where("status = ?", p.Status)
}
if err := q.Count(&total).Error; err != nil {
span.RecordError(err)
return nil, 0, err
}
if err := q.Offset((p.Page - 1) * p.Size).Limit(p.Size).Order("id DESC").Find(&list).Error; err != nil {
span.RecordError(err)
return nil, 0, err
}
return list, total, nil
}
// ClosetShareGetByUUID 按分享 UUID 查询
func (d *ClosetShareDao) ClosetShareGetByUUID(ctx context.Context, shareUUID string) (*mysqlmodel.ClosetShare, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.ClosetShareGetByUUID")
defer span.End()
var share mysqlmodel.ClosetShare
if err := d.db.WithContext(ctx).Where("share_uuid = ?", shareUUID).First(&share).Error; err != nil {
span.RecordError(err)
return nil, err
}
return &share, nil
}
// ClosetShareUpdateStatus 更新分享状态(封禁/解封)
func (d *ClosetShareDao) ClosetShareUpdateStatus(ctx context.Context, shareUUID string, status int8) error {
ctx, span := global.Tracer.Start(ctx, "mysqldao.ClosetShareUpdateStatus")
defer span.End()
if err := d.db.WithContext(ctx).Model(&mysqlmodel.ClosetShare{}).
Where("share_uuid = ?", shareUUID).
UpdateColumn("status", status).Error; err != nil {
span.RecordError(err)
return err
}
return nil
}
+89
View File
@@ -0,0 +1,89 @@
package mysqldao
import (
"context"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqlmodel"
"gorm.io/gorm"
)
// ClothesDao 衣物数据访问
type ClothesDao struct {
db *gorm.DB
}
// NewClothesDao 创建衣物 DAO
func NewClothesDao() *ClothesDao {
return &ClothesDao{db: global.MysqlEngine}
}
// ClothesListParams 衣物分页列表筛选参数。
type ClothesListParams struct {
UserID int64 // 用户 ID,0 表示全部
ImageProcessStatus int8 // AI 处理状态,0 表示全部
CategoryMain string // 大类筛选
Keyword string // 模糊匹配名称
Page int
Size int
}
// ClothesList 分页查询衣物
func (d *ClothesDao) ClothesList(ctx context.Context, p ClothesListParams) ([]mysqlmodel.Clothes, int64, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.ClothesList")
defer span.End()
var list []mysqlmodel.Clothes
var total int64
q := d.db.WithContext(ctx).Model(&mysqlmodel.Clothes{})
if p.UserID != 0 {
q = q.Where("user_id = ?", p.UserID)
}
if p.ImageProcessStatus != 0 {
q = q.Where("image_process_status = ?", p.ImageProcessStatus)
}
if p.CategoryMain != "" {
q = q.Where("category_main = ?", p.CategoryMain)
}
if p.Keyword != "" {
q = q.Where("name LIKE ?", "%"+p.Keyword+"%")
}
if err := q.Count(&total).Error; err != nil {
span.RecordError(err)
return nil, 0, err
}
if err := q.Offset((p.Page - 1) * p.Size).Limit(p.Size).Order("id DESC").Find(&list).Error; err != nil {
span.RecordError(err)
return nil, 0, err
}
return list, total, nil
}
// ClothesGetByUUID 按衣物 UUID 查询
func (d *ClothesDao) ClothesGetByUUID(ctx context.Context, clothesUUID string) (*mysqlmodel.Clothes, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.ClothesGetByUUID")
defer span.End()
var clothes mysqlmodel.Clothes
if err := d.db.WithContext(ctx).Where("clothes_uuid = ?", clothesUUID).First(&clothes).Error; err != nil {
span.RecordError(err)
return nil, err
}
return &clothes, nil
}
// ClothesListByIDs 按主键 ID 批量查询衣物(穿搭详情聚合用)
func (d *ClothesDao) ClothesListByIDs(ctx context.Context, ids []int64) ([]mysqlmodel.Clothes, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.ClothesListByIDs")
defer span.End()
if len(ids) == 0 {
return []mysqlmodel.Clothes{}, nil
}
var list []mysqlmodel.Clothes
if err := d.db.WithContext(ctx).Where("id IN ?", ids).Find(&list).Error; err != nil {
span.RecordError(err)
return nil, err
}
return list, nil
}
+10
View File
@@ -0,0 +1,10 @@
package mysqldao
import (
"github.com/google/uuid"
)
// genFlowUUID 生成流水 UUID(人工调整流水 order_no 兜底用)。
func genFlowUUID() string {
return uuid.NewString()
}
+119
View File
@@ -0,0 +1,119 @@
package mysqldao
import (
"context"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqlmodel"
"gorm.io/gorm"
)
// GoodsDao 商品数据访问
type GoodsDao struct {
db *gorm.DB
}
// NewGoodsDao 创建商品 DAO
func NewGoodsDao() *GoodsDao {
return &GoodsDao{db: global.MysqlEngine}
}
// GoodsListParams 商品分页列表筛选参数。
type GoodsListParams struct {
GoodsType int8 // 商品类型筛选,0 表示全部
Status int8 // 状态筛选,0 表示全部(含下架)
Keyword string // 模糊匹配商品名称/编码
Page int
Size int
}
// GoodsList 分页查询商品(管理端含下架商品)
func (d *GoodsDao) GoodsList(ctx context.Context, p GoodsListParams) ([]mysqlmodel.Goods, int64, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.GoodsList")
defer span.End()
var list []mysqlmodel.Goods
var total int64
q := d.db.WithContext(ctx).Model(&mysqlmodel.Goods{})
if p.GoodsType != 0 {
q = q.Where("goods_type = ?", p.GoodsType)
}
if p.Status != 0 {
q = q.Where("status = ?", p.Status)
}
if p.Keyword != "" {
kw := "%" + p.Keyword + "%"
q = q.Where("goods_name LIKE ? OR goods_code LIKE ?", kw, kw)
}
if err := q.Count(&total).Error; err != nil {
span.RecordError(err)
return nil, 0, err
}
if err := q.Offset((p.Page - 1) * p.Size).Limit(p.Size).Order("id DESC").Find(&list).Error; err != nil {
span.RecordError(err)
return nil, 0, err
}
return list, total, nil
}
// GoodsGetByID 按主键 ID 查询商品
func (d *GoodsDao) GoodsGetByID(ctx context.Context, id int64) (*mysqlmodel.Goods, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.GoodsGetByID")
defer span.End()
var goods mysqlmodel.Goods
if err := d.db.WithContext(ctx).First(&goods, id).Error; err != nil {
span.RecordError(err)
return nil, err
}
return &goods, nil
}
// GoodsGetByCode 按商品编码查询商品(含软删,用于编码唯一性校验)
func (d *GoodsDao) GoodsGetByCode(ctx context.Context, code string) (*mysqlmodel.Goods, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.GoodsGetByCode")
defer span.End()
var goods mysqlmodel.Goods
if err := d.db.WithContext(ctx).Unscoped().Where("goods_code = ?", code).First(&goods).Error; err != nil {
span.RecordError(err)
return nil, err
}
return &goods, nil
}
// GoodsCreate 创建商品
func (d *GoodsDao) GoodsCreate(ctx context.Context, goods *mysqlmodel.Goods) error {
ctx, span := global.Tracer.Start(ctx, "mysqldao.GoodsCreate")
defer span.End()
if err := d.db.WithContext(ctx).Create(goods).Error; err != nil {
span.RecordError(err)
return err
}
return nil
}
// GoodsUpdate 按主键更新商品字段
func (d *GoodsDao) GoodsUpdate(ctx context.Context, id int64, updates map[string]any) error {
ctx, span := global.Tracer.Start(ctx, "mysqldao.GoodsUpdate")
defer span.End()
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Goods{}).Where("id = ?", id).Updates(updates).Error; err != nil {
span.RecordError(err)
return err
}
return nil
}
// GoodsSoftDelete 软删除商品
func (d *GoodsDao) GoodsSoftDelete(ctx context.Context, id int64) error {
ctx, span := global.Tracer.Start(ctx, "mysqldao.GoodsSoftDelete")
defer span.End()
if err := d.db.WithContext(ctx).Where("id = ?", id).Delete(&mysqlmodel.Goods{}).Error; err != nil {
span.RecordError(err)
return err
}
return nil
}
+34
View File
@@ -0,0 +1,34 @@
package mysqldao
import (
"context"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqlmodel"
"gorm.io/gorm"
)
// NotificationDao 通知数据访问
type NotificationDao struct {
db *gorm.DB
}
// NewNotificationDao 创建通知 DAO
func NewNotificationDao() *NotificationDao {
return &NotificationDao{db: global.MysqlEngine}
}
// NotificationBatchCreate 批量插入通知(单用户/分片广播共用)
func (d *NotificationDao) NotificationBatchCreate(ctx context.Context, notifications []mysqlmodel.UserNotification) error {
ctx, span := global.Tracer.Start(ctx, "mysqldao.NotificationBatchCreate")
defer span.End()
if len(notifications) == 0 {
return nil
}
if err := d.db.WithContext(ctx).CreateInBatches(notifications, 500).Error; err != nil {
span.RecordError(err)
return err
}
return nil
}
+82
View File
@@ -0,0 +1,82 @@
package mysqldao
import (
"context"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqlmodel"
"gorm.io/gorm"
)
// OrderDao 订单数据访问
type OrderDao struct {
db *gorm.DB
}
// NewOrderDao 创建订单 DAO
func NewOrderDao() *OrderDao {
return &OrderDao{db: global.MysqlEngine}
}
// OrderListParams 订单分页列表筛选参数。
type OrderListParams struct {
OrderNo string // 订单号(精确)
UserID int64 // 用户 ID,0 表示全部
Status int8 // 订单状态,0 表示全部
Page int
Size int
}
// OrderList 分页查询订单
func (d *OrderDao) OrderList(ctx context.Context, p OrderListParams) ([]mysqlmodel.Order, int64, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.OrderList")
defer span.End()
var list []mysqlmodel.Order
var total int64
q := d.db.WithContext(ctx).Model(&mysqlmodel.Order{})
if p.OrderNo != "" {
q = q.Where("order_no = ?", p.OrderNo)
}
if p.UserID != 0 {
q = q.Where("user_id = ?", p.UserID)
}
if p.Status != 0 {
q = q.Where("order_status = ?", p.Status)
}
if err := q.Count(&total).Error; err != nil {
span.RecordError(err)
return nil, 0, err
}
if err := q.Offset((p.Page - 1) * p.Size).Limit(p.Size).Order("id DESC").Find(&list).Error; err != nil {
span.RecordError(err)
return nil, 0, err
}
return list, total, nil
}
// OrderGetByOrderNo 按订单号查询订单
func (d *OrderDao) OrderGetByOrderNo(ctx context.Context, orderNo string) (*mysqlmodel.Order, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.OrderGetByOrderNo")
defer span.End()
var order mysqlmodel.Order
if err := d.db.WithContext(ctx).Where("order_no = ?", orderNo).First(&order).Error; err != nil {
span.RecordError(err)
return nil, err
}
return &order, nil
}
// OrderGetByID 按主键 ID 查询订单
func (d *OrderDao) OrderGetByID(ctx context.Context, id int64) (*mysqlmodel.Order, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.OrderGetByID")
defer span.End()
var order mysqlmodel.Order
if err := d.db.WithContext(ctx).First(&order, id).Error; err != nil {
span.RecordError(err)
return nil, err
}
return &order, nil
}
+120
View File
@@ -0,0 +1,120 @@
package mysqldao
import (
"context"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqlmodel"
"gorm.io/gorm"
)
// OutfitDao 穿搭数据访问
type OutfitDao struct {
db *gorm.DB
}
// NewOutfitDao 创建穿搭 DAO
func NewOutfitDao() *OutfitDao {
return &OutfitDao{db: global.MysqlEngine}
}
// OutfitListParams 穿搭分页列表筛选参数。
type OutfitListParams struct {
UserID int64 // 用户 ID,0 表示全部
IsPublic int8 // 是否公开,0 表示全部
AuditStatus int8 // 审核状态,0 表示全部
Status int8 // 穿搭状态,0 表示全部
Keyword string // 模糊匹配穿搭名称
Page int
Size int
}
// OutfitList 分页查询穿搭(管理端全量视角)
func (d *OutfitDao) OutfitList(ctx context.Context, p OutfitListParams) ([]mysqlmodel.Outfit, int64, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.OutfitList")
defer span.End()
var list []mysqlmodel.Outfit
var total int64
q := d.db.WithContext(ctx).Model(&mysqlmodel.Outfit{})
if p.UserID != 0 {
q = q.Where("user_id = ?", p.UserID)
}
if p.IsPublic != 0 {
q = q.Where("is_public = ?", p.IsPublic)
}
if p.AuditStatus != 0 {
q = q.Where("audit_status = ?", p.AuditStatus)
}
if p.Status != 0 {
q = q.Where("status = ?", p.Status)
}
if p.Keyword != "" {
q = q.Where("outfit_name LIKE ?", "%"+p.Keyword+"%")
}
if err := q.Count(&total).Error; err != nil {
span.RecordError(err)
return nil, 0, err
}
if err := q.Offset((p.Page - 1) * p.Size).Limit(p.Size).Order("id DESC").Find(&list).Error; err != nil {
span.RecordError(err)
return nil, 0, err
}
return list, total, nil
}
// OutfitGetByUUID 按穿搭 UUID 查询
func (d *OutfitDao) OutfitGetByUUID(ctx context.Context, outfitUUID string) (*mysqlmodel.Outfit, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.OutfitGetByUUID")
defer span.End()
var outfit mysqlmodel.Outfit
if err := d.db.WithContext(ctx).Where("outfit_uuid = ?", outfitUUID).First(&outfit).Error; err != nil {
span.RecordError(err)
return nil, err
}
return &outfit, nil
}
// OutfitItemListByOutfitID 查询穿搭衣物明细
func (d *OutfitDao) OutfitItemListByOutfitID(ctx context.Context, outfitID int64) ([]mysqlmodel.OutfitItem, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.OutfitItemListByOutfitID")
defer span.End()
var items []mysqlmodel.OutfitItem
if err := d.db.WithContext(ctx).Where("outfit_id = ?", outfitID).Find(&items).Error; err != nil {
span.RecordError(err)
return nil, err
}
return items, nil
}
// OutfitUpdateByID 按主键更新穿搭字段
func (d *OutfitDao) OutfitUpdateByID(ctx context.Context, id int64, updates map[string]any) error {
ctx, span := global.Tracer.Start(ctx, "mysqldao.OutfitUpdateByID")
defer span.End()
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Outfit{}).Where("id = ?", id).Updates(updates).Error; err != nil {
span.RecordError(err)
return err
}
return nil
}
// OutfitAuditWithLog 事务内更新穿搭审核状态并写审核日志。
func (d *OutfitDao) OutfitAuditWithLog(ctx context.Context, outfitID int64, auditResult int8, rejectReason string, log *mysqlmodel.OutfitAuditLog) error {
ctx, span := global.Tracer.Start(ctx, "mysqldao.OutfitAuditWithLog")
defer span.End()
return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&mysqlmodel.Outfit{}).
Where("id = ?", outfitID).
UpdateColumn("audit_status", auditResult).Error; err != nil {
return err
}
log.TargetID = outfitID
log.AuditResult = auditResult
log.RejectReason = rejectReason
return tx.Create(log).Error
})
}
+101
View File
@@ -0,0 +1,101 @@
package mysqldao
import (
"context"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqlmodel"
"gorm.io/gorm"
)
// QuotaGrantDao 额度凭证数据访问(有有效期赠送额度)。
type QuotaGrantDao struct {
db *gorm.DB
}
// NewQuotaGrantDao 创建额度凭证 DAO。
func NewQuotaGrantDao() *QuotaGrantDao {
return &QuotaGrantDao{db: global.MysqlEngine}
}
// QuotaGrantCreate 创建额度凭证(ref_id+quota_type 唯一索引兜底幂等,冲突返回 1062)。
func (d *QuotaGrantDao) QuotaGrantCreate(ctx context.Context, grant *mysqlmodel.QuotaGrant) error {
ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaGrantCreate")
defer span.End()
if err := d.db.WithContext(ctx).Create(grant).Error; err != nil {
span.RecordError(err)
return err
}
return nil
}
// QuotaGrantListByUserID 查询用户全部凭证(未删除),按 id 倒序。
func (d *QuotaGrantDao) QuotaGrantListByUserID(ctx context.Context, userID int64) ([]mysqlmodel.QuotaGrant, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaGrantListByUserID")
defer span.End()
var list []mysqlmodel.QuotaGrant
if err := d.db.WithContext(ctx).Where("user_id = ?", userID).Order("id DESC").Find(&list).Error; err != nil {
span.RecordError(err)
return nil, err
}
return list, nil
}
// QuotaGrantGetByID 按主键查询凭证(未删除)。
func (d *QuotaGrantDao) QuotaGrantGetByID(ctx context.Context, id int64) (*mysqlmodel.QuotaGrant, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaGrantGetByID")
defer span.End()
var grant mysqlmodel.QuotaGrant
if err := d.db.WithContext(ctx).First(&grant, id).Error; err != nil {
span.RecordError(err)
return nil, err
}
return &grant, nil
}
// QuotaGrantSoftDelete 软删除凭证(按 id + user_id 归属校验)。
func (d *QuotaGrantDao) QuotaGrantSoftDelete(ctx context.Context, id, userID int64) error {
ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaGrantSoftDelete")
defer span.End()
if err := d.db.WithContext(ctx).
Where("id = ? AND user_id = ?", id, userID).
Delete(&mysqlmodel.QuotaGrant{}).Error; err != nil {
span.RecordError(err)
return err
}
return nil
}
// QuotaGetByUserIDAndType 查询用户某类配额(Cos 永久额度的已用/底值)。
func (d *QuotaGrantDao) QuotaGetByUserIDAndType(ctx context.Context, userID int64, quotaType int8) (*mysqlmodel.Quota, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaGetByUserIDAndType")
defer span.End()
var quota mysqlmodel.Quota
if err := d.db.WithContext(ctx).
Where("user_id = ? AND quota_type = ?", userID, quotaType).
First(&quota).Error; err != nil {
span.RecordError(err)
return nil, err
}
return &quota, nil
}
// UserDailyUsageGetByUserIDAndDate 查询用户某日使用计数(当日额度已用,Redis 异常时 MySQL 兜底)。
func (d *QuotaGrantDao) UserDailyUsageGetByUserIDAndDate(ctx context.Context, userID, statDate int64) (*mysqlmodel.UserDailyUsage, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.UserDailyUsageGetByUserIDAndDate")
defer span.End()
var usage mysqlmodel.UserDailyUsage
if err := d.db.WithContext(ctx).
Where("user_id = ? AND stat_date = ?", userID, statDate).
First(&usage).Error; err != nil {
span.RecordError(err)
return nil, err
}
return &usage, nil
}
+85
View File
@@ -0,0 +1,85 @@
package mysqldao
import (
"context"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqlmodel"
"gorm.io/gorm"
)
// RefundDao 退款单数据访问
type RefundDao struct {
db *gorm.DB
}
// NewRefundDao 创建退款 DAO
func NewRefundDao() *RefundDao {
return &RefundDao{db: global.MysqlEngine}
}
// RefundListParams 退款单分页列表筛选参数。
type RefundListParams struct {
Status int8 // 退款状态(WithStatus 为 true 时生效)
WithStatus bool // 是否按状态筛选(退款状态 0=待处理 是合法值,需显式区分「不过滤」)
OrderNo string // 订单号(精确)
Page int
Size int
}
// RefundList 分页查询退款单
func (d *RefundDao) RefundList(ctx context.Context, p RefundListParams) ([]mysqlmodel.RefundFlowLog, int64, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.RefundList")
defer span.End()
var list []mysqlmodel.RefundFlowLog
var total int64
q := d.db.WithContext(ctx).Model(&mysqlmodel.RefundFlowLog{})
if p.WithStatus {
q = q.Where("status = ?", p.Status)
}
if p.OrderNo != "" {
q = q.Where("order_no = ?", p.OrderNo)
}
if err := q.Count(&total).Error; err != nil {
span.RecordError(err)
return nil, 0, err
}
if err := q.Offset((p.Page - 1) * p.Size).Limit(p.Size).Order("id DESC").Find(&list).Error; err != nil {
span.RecordError(err)
return nil, 0, err
}
return list, total, nil
}
// RefundGetByRefundNo 按退款单号查询
func (d *RefundDao) RefundGetByRefundNo(ctx context.Context, refundNo string) (*mysqlmodel.RefundFlowLog, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.RefundGetByRefundNo")
defer span.End()
var refund mysqlmodel.RefundFlowLog
if err := d.db.WithContext(ctx).Where("refund_no = ?", refundNo).First(&refund).Error; err != nil {
span.RecordError(err)
return nil, err
}
return &refund, nil
}
// RefundTransition 条件更新退款状态(仅 from 状态可流转到 to,行锁防并发重复审核)。
func (d *RefundDao) RefundTransition(ctx context.Context, refundNo string, from, to int8, failReason string) (bool, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.RefundTransition")
defer span.End()
updates := map[string]any{"status": to}
if failReason != "" {
updates["fail_reason"] = failReason
}
res := d.db.WithContext(ctx).Model(&mysqlmodel.RefundFlowLog{}).
Where("refund_no = ? AND status = ?", refundNo, from).
Updates(updates)
if res.Error != nil {
span.RecordError(res.Error)
return false, res.Error
}
return res.RowsAffected > 0, nil
}
+518
View File
@@ -0,0 +1,518 @@
package mysqldao
import (
"context"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqlmodel"
"gorm.io/gorm"
)
// StatsDao 仪表盘聚合统计数据访问
type StatsDao struct {
db *gorm.DB
}
// NewStatsDao 创建统计 DAO
func NewStatsDao() *StatsDao {
return &StatsDao{db: global.MysqlEngine}
}
// UserStats 用户统计。
type UserStats struct {
Total int64 // 用户总数
TodayNew int64 // 今日新增
Normal int64 // 正常
TempBan int64 // 临时禁用
PermBan int64 // 永久禁用
Deactivated int64 // 已注销
}
// OrderStats 订单统计。
type OrderStats struct {
Total int64 // 订单总数
Pending int64 // 待支付
Paid int64 // 已支付
Cancelled int64 // 已取消
Expired int64 // 已过期
Refunded int64 // 已退款
}
// RevenueStats 营收统计(单位分)。
type RevenueStats struct {
TotalRevenue int64 // 累计营收(已支付+已退款订单实付金额)
TodayRevenue int64 // 今日营收(今日支付订单实付金额)
RefundTotal int64 // 累计退款金额
}
// TodoStats 待办统计。
type TodoStats struct {
RefundPending int64 // 待处理退款数
OutfitPendingAudit int64 // 待审核穿搭数
GoodsOnShelf int64 // 上架商品数
}
// UserStatsGet 聚合用户总数与今日新增
func (d *StatsDao) UserStatsGet(ctx context.Context, todayStart int64) (*UserStats, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.UserStatsGet")
defer span.End()
s := &UserStats{}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}).Count(&s.Total).Error; err != nil {
span.RecordError(err)
return nil, err
}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}).Where("created_at >= ?", todayStart).Count(&s.TodayNew).Error; err != nil {
span.RecordError(err)
return nil, err
}
return s, nil
}
// UserStatsGetByStatus 按状态统计用户数(单次 group by)
func (d *StatsDao) UserStatsGetByStatus(ctx context.Context) (map[int8]int64, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.UserStatsGetByStatus")
defer span.End()
rows := []struct {
Status int8
Cnt int64
}{}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}).
Select("status, COUNT(*) AS cnt").
Group("status").
Scan(&rows).Error; err != nil {
span.RecordError(err)
return nil, err
}
m := make(map[int8]int64, len(rows))
for _, r := range rows {
m[r.Status] = r.Cnt
}
return m, nil
}
// OrderStatsGet 聚合订单状态统计(单次 group by)
func (d *StatsDao) OrderStatsGet(ctx context.Context) (*OrderStats, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.OrderStatsGet")
defer span.End()
rows := []struct {
OrderStatus int8
Cnt int64
}{}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Order{}).
Select("order_status, COUNT(*) AS cnt").
Group("order_status").
Scan(&rows).Error; err != nil {
span.RecordError(err)
return nil, err
}
s := &OrderStats{}
for _, r := range rows {
s.Total += r.Cnt
switch r.OrderStatus {
case 1:
s.Pending = r.Cnt
case 2:
s.Paid = r.Cnt
case 3:
s.Cancelled = r.Cnt
case 4:
s.Expired = r.Cnt
case 5:
s.Refunded = r.Cnt
}
}
return s, nil
}
// RevenueStatsGet 聚合营收统计(已支付与已退款订单均计营收;今日按支付时间统计)
func (d *StatsDao) RevenueStatsGet(ctx context.Context, todayStart int64) (*RevenueStats, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.RevenueStatsGet")
defer span.End()
s := &RevenueStats{}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Order{}).
Where("order_status IN ?", []int8{2, 5}).
Select("COALESCE(SUM(pay_amount), 0)").
Scan(&s.TotalRevenue).Error; err != nil {
span.RecordError(err)
return nil, err
}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Order{}).
Where("order_status IN ? AND pay_time >= ?", []int8{2, 5}, todayStart).
Select("COALESCE(SUM(pay_amount), 0)").
Scan(&s.TodayRevenue).Error; err != nil {
span.RecordError(err)
return nil, err
}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Order{}).
Where("order_status = ?", int8(5)).
Select("COALESCE(SUM(refund_amount), 0)").
Scan(&s.RefundTotal).Error; err != nil {
span.RecordError(err)
return nil, err
}
return s, nil
}
// TodoStatsGet 聚合待办统计
func (d *StatsDao) TodoStatsGet(ctx context.Context) (*TodoStats, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.TodoStatsGet")
defer span.End()
s := &TodoStats{}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.RefundFlowLog{}).
Where("status = ?", int8(0)).
Count(&s.RefundPending).Error; err != nil {
span.RecordError(err)
return nil, err
}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Outfit{}).
Where("is_public = ? AND audit_status = ?", int8(2), int8(1)).
Count(&s.OutfitPendingAudit).Error; err != nil {
span.RecordError(err)
return nil, err
}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Goods{}).
Where("status = ?", int8(1)).
Count(&s.GoodsOnShelf).Error; err != nil {
span.RecordError(err)
return nil, err
}
return s, nil
}
// countByInt8 通用分组计数行(小整型枚举 + 计数)。
type countByInt8 struct {
K int8
Cnt int64
}
// dateCount 按日分组计数行。
type dateCount struct {
D string
Cnt int64
}
// dateRevenue 按日分组营收行。
type dateRevenue struct {
D string
Cnt int64
Rev int64
}
// ContentStats 内容审核统计。
type ContentStats struct {
OutfitTotal int64
OutfitPending int64
OutfitPassed int64
OutfitRejected int64
OutfitBanned int64
ClothesTotal int64
ClothesProcessing int64
ClothesSuccess int64
ClothesFailed int64
ShareTotal int64
ShareNormal int64
ShareCancelled int64
ShareExpired int64
ShareBanned int64
}
// VIPStats 会员统计。
type VIPStats struct {
MemberTotal int64
Monthly int64
Yearly int64
Trial int64
ExpiringSoon int64
}
// GoodsTypeCount 商品类型数量。
type GoodsTypeCount struct {
Type int8
Count int64
}
// GoodsStats 商品统计。
type GoodsStats struct {
Total int64
OnShelf int64
OffShelf int64
ByType []GoodsTypeCount
}
// TrendStats 趋势原始聚合(按 YYYY-MM-DD 分组)。
type TrendStats struct {
NewUsers map[string]int64 // 新增用户
Orders map[string]int64 // 已支付订单量
Revenue map[string]int64 // 营收(分)
}
// ContentStatsGet 聚合内容审核概况:穿搭/衣物/衣柜分享。
func (d *StatsDao) ContentStatsGet(ctx context.Context) (*ContentStats, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.ContentStatsGet")
defer span.End()
s := &ContentStats{}
// 穿搭:audit_status 分布 + 封禁数
outfitRows := []countByInt8{}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Outfit{}).
Select("audit_status AS k, COUNT(*) AS cnt").
Group("audit_status").
Scan(&outfitRows).Error; err != nil {
span.RecordError(err)
return nil, err
}
for _, r := range outfitRows {
s.OutfitTotal += r.Cnt
switch r.K {
case 1:
s.OutfitPending = r.Cnt
case 2:
s.OutfitPassed = r.Cnt
case 3:
s.OutfitRejected = r.Cnt
}
}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Outfit{}).
Where("status = ?", int8(2)).
Count(&s.OutfitBanned).Error; err != nil {
span.RecordError(err)
return nil, err
}
// 衣物:image_process_status 分布(1/2/3 处理中,4 成功,5 失败)
clothesRows := []countByInt8{}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Clothes{}).
Select("image_process_status AS k, COUNT(*) AS cnt").
Group("image_process_status").
Scan(&clothesRows).Error; err != nil {
span.RecordError(err)
return nil, err
}
for _, r := range clothesRows {
s.ClothesTotal += r.Cnt
switch r.K {
case 1, 2, 3:
s.ClothesProcessing += r.Cnt
case 4:
s.ClothesSuccess = r.Cnt
case 5:
s.ClothesFailed = r.Cnt
}
}
// 衣柜分享:status 分布
shareRows := []countByInt8{}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.ClosetShare{}).
Select("status AS k, COUNT(*) AS cnt").
Group("status").
Scan(&shareRows).Error; err != nil {
span.RecordError(err)
return nil, err
}
for _, r := range shareRows {
s.ShareTotal += r.Cnt
switch r.K {
case 1:
s.ShareNormal = r.Cnt
case 2:
s.ShareCancelled = r.Cnt
case 3:
s.ShareExpired = r.Cnt
case 4:
s.ShareBanned = r.Cnt
}
}
return s, nil
}
// VIPStatsGet 聚合会员概况(有效会员 + 即将到期)。
func (d *StatsDao) VIPStatsGet(ctx context.Context, now, soonThreshold int64) (*VIPStats, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.VIPStatsGet")
defer span.End()
s := &VIPStats{}
activeVIP := []int8{2, 3, 4}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}).
Where("vip_type IN ? AND vip_expire > ?", activeVIP, now).
Count(&s.MemberTotal).Error; err != nil {
span.RecordError(err)
return nil, err
}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}).
Where("vip_type = ? AND vip_expire > ?", int8(2), now).
Count(&s.Monthly).Error; err != nil {
span.RecordError(err)
return nil, err
}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}).
Where("vip_type = ? AND vip_expire > ?", int8(3), now).
Count(&s.Yearly).Error; err != nil {
span.RecordError(err)
return nil, err
}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}).
Where("vip_type = ? AND vip_expire > ?", int8(4), now).
Count(&s.Trial).Error; err != nil {
span.RecordError(err)
return nil, err
}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}).
Where("vip_type IN ? AND vip_expire >= ? AND vip_expire <= ?", activeVIP, now, soonThreshold).
Count(&s.ExpiringSoon).Error; err != nil {
span.RecordError(err)
return nil, err
}
return s, nil
}
// GoodsStatsGet 聚合商品统计(上架/下架 + 类型分布)。
func (d *StatsDao) GoodsStatsGet(ctx context.Context) (*GoodsStats, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.GoodsStatsGet")
defer span.End()
s := &GoodsStats{}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Goods{}).Count(&s.Total).Error; err != nil {
span.RecordError(err)
return nil, err
}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Goods{}).
Where("status = ?", int8(1)).
Count(&s.OnShelf).Error; err != nil {
span.RecordError(err)
return nil, err
}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Goods{}).
Where("status = ?", int8(2)).
Count(&s.OffShelf).Error; err != nil {
span.RecordError(err)
return nil, err
}
rows := []countByInt8{}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Goods{}).
Select("goods_type AS k, COUNT(*) AS cnt").
Group("goods_type").
Scan(&rows).Error; err != nil {
span.RecordError(err)
return nil, err
}
for _, r := range rows {
s.ByType = append(s.ByType, GoodsTypeCount{Type: r.K, Count: r.Cnt})
}
return s, nil
}
// TrendStatsGet 聚合近 N 天趋势(新增用户/已支付订单量/营收,按天分组)。
func (d *StatsDao) TrendStatsGet(ctx context.Context, startTs int64) (*TrendStats, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.TrendStatsGet")
defer span.End()
s := &TrendStats{
NewUsers: map[string]int64{},
Orders: map[string]int64{},
Revenue: map[string]int64{},
}
// 新增用户:按创建时间
userRows := []dateCount{}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}).
Where("created_at >= ?", startTs).
Select("DATE_FORMAT(FROM_UNIXTIME(created_at), '%Y-%m-%d') AS d, COUNT(*) AS cnt").
Group("d").
Scan(&userRows).Error; err != nil {
span.RecordError(err)
return nil, err
}
for _, r := range userRows {
s.NewUsers[r.D] = r.Cnt
}
// 已支付/已退款订单量 + 营收:按支付时间
orderRows := []dateRevenue{}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Order{}).
Where("order_status IN ? AND pay_time >= ?", []int8{2, 5}, startTs).
Select("DATE_FORMAT(FROM_UNIXTIME(pay_time), '%Y-%m-%d') AS d, COUNT(*) AS cnt, COALESCE(SUM(pay_amount), 0) AS rev").
Group("d").
Scan(&orderRows).Error; err != nil {
span.RecordError(err)
return nil, err
}
for _, r := range orderRows {
s.Orders[r.D] = r.Cnt
s.Revenue[r.D] = r.Rev
}
return s, nil
}
// UsageStats 今日消耗统计。
type UsageStats struct {
AIRecommend int64 // 今日AI推荐消耗次数
ClosetShare int64 // 今日衣柜分享消耗次数
OutfitShare int64 // 今日社区分享消耗次数
}
// SocialStats 内容互动统计。
type SocialStats struct {
CosImageCount int64 // COS总图数
LikeCount int64 // 点赞数
CollectCount int64 // 收藏数
}
// UsageStatsGet 聚合今日消耗次数(AI推荐/衣柜分享/社区分享,按 stat_date 单次求和)。
func (d *StatsDao) UsageStatsGet(ctx context.Context, statDate int64) (*UsageStats, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.UsageStatsGet")
defer span.End()
s := &UsageStats{}
row := struct {
AIRecommend int64
ClosetShare int64
OutfitShare int64
}{}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.UserDailyUsage{}).
Where("stat_date = ?", statDate).
Select("COALESCE(SUM(ai_recommend_count), 0) AS ai_recommend, COALESCE(SUM(closet_share_count), 0) AS closet_share, COALESCE(SUM(outfit_share_count), 0) AS outfit_share").
Scan(&row).Error; err != nil {
span.RecordError(err)
return nil, err
}
s.AIRecommend = row.AIRecommend
s.ClosetShare = row.ClosetShare
s.OutfitShare = row.OutfitShare
return s, nil
}
// SocialStatsGet 聚合内容互动统计(COS 总图数/点赞数/收藏数)。
func (d *StatsDao) SocialStatsGet(ctx context.Context) (*SocialStats, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.SocialStatsGet")
defer span.End()
s := &SocialStats{}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Clothes{}).
Where("processed_image <> ''").
Count(&s.CosImageCount).Error; err != nil {
span.RecordError(err)
return nil, err
}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Like{}).
Where("status = ?", int8(1)).
Count(&s.LikeCount).Error; err != nil {
span.RecordError(err)
return nil, err
}
if err := d.db.WithContext(ctx).Model(&mysqlmodel.Collect{}).
Where("status = ?", int8(1)).
Count(&s.CollectCount).Error; err != nil {
span.RecordError(err)
return nil, err
}
return s, nil
}
+144
View File
@@ -0,0 +1,144 @@
package mysqldao
import (
"context"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/global"
"git.lzcwy.cn/zhengchiliu/zt-closet-manage-backend/internal/mysqlmodel"
"gorm.io/gorm"
)
// UserDao 用户数据访问
type UserDao struct {
db *gorm.DB
}
// NewUserDao 创建用户 DAO
func NewUserDao() *UserDao {
return &UserDao{db: global.MysqlEngine}
}
// UserListParams 用户分页列表筛选参数。
type UserListParams struct {
Keyword string // 模糊匹配昵称/手机号/OpenID(任一命中)
Status int8 // 状态筛选,0 表示全部
Page int
Size int
}
// UserList 分页查询用户(关键词 + 状态筛选)
func (d *UserDao) UserList(ctx context.Context, p UserListParams) ([]mysqlmodel.User, int64, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.UserList")
defer span.End()
var list []mysqlmodel.User
var total int64
q := d.db.WithContext(ctx).Model(&mysqlmodel.User{})
if p.Keyword != "" {
kw := "%" + p.Keyword + "%"
q = q.Where("username LIKE ? OR phone LIKE ? OR wx_open_id LIKE ?", kw, kw, kw)
}
if p.Status != 0 {
q = q.Where("status = ?", p.Status)
}
if err := q.Count(&total).Error; err != nil {
span.RecordError(err)
return nil, 0, err
}
if err := q.Offset((p.Page - 1) * p.Size).Limit(p.Size).Order("id DESC").Find(&list).Error; err != nil {
span.RecordError(err)
return nil, 0, err
}
return list, total, nil
}
// 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
}
// 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
}
// 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
}
// QuotaListByUserID 查询用户的所有配额
func (d *UserDao) QuotaListByUserID(ctx context.Context, userID int64) ([]mysqlmodel.Quota, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.QuotaListByUserID")
defer span.End()
var quotas []mysqlmodel.Quota
if err := d.db.WithContext(ctx).Where("user_id = ?", userID).Find(&quotas).Error; err != nil {
span.RecordError(err)
return nil, err
}
return quotas, nil
}
// VIPAdjustWithFlow 事务内人工调整用户 VIP 并写流水。
func (d *UserDao) VIPAdjustWithFlow(ctx context.Context, user *mysqlmodel.User, flow *mysqlmodel.VIPFlowLog) error {
ctx, span := global.Tracer.Start(ctx, "mysqldao.VIPAdjustWithFlow")
defer span.End()
return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&mysqlmodel.User{}).
Where("id = ?", user.ID).
Updates(map[string]any{
"vip_type": user.VIPType,
"vip_start": user.VIPStart,
"vip_expire": user.VIPExpire,
}).Error; err != nil {
return err
}
// 人工调整无订单号,用流水 UUID 填充 order_no 满足联合唯一索引。
if flow.OrderNo == "" {
flow.OrderNo = genFlowUUID()
}
return tx.Create(flow).Error
})
}
// UserIDListAll 分批遍历全部用户 ID(通知广播用)。
func (d *UserDao) UserIDListAll(ctx context.Context, lastID int64, limit int) ([]int64, error) {
ctx, span := global.Tracer.Start(ctx, "mysqldao.UserIDListAll")
defer span.End()
var ids []int64
if err := d.db.WithContext(ctx).Model(&mysqlmodel.User{}).
Where("id > ?", lastID).
Order("id ASC").
Limit(limit).
Pluck("id", &ids).Error; err != nil {
span.RecordError(err)
return nil, err
}
return ids, nil
}