Files
zt-closet-backend/.codebuddy/plans/补齐_mysqldao_数据访问层_a1ecd74f.md
2026-09-11 01:41:52 +08:00

12 KiB
Raw Permalink Blame History

name, overview, todos
name overview todos
补齐 mysqldao 数据访问层 根据 mysqlmodel 全部实体与 dto 暴露的业务接口,补齐 internal/mysqldao 下所有 DAO(单表 CRUD + 常用查询,含分页、软删除、原子扣减、幂等),沿用现有 user.go 风格。
id content status
fix-user-dao 修正 user.go 统一 d.db 并移除 UserQuotaListByUserID completed
id content status
add-clothes-outfit-closet-dao 新增 clothes.go、outfit.go、closet.go 三个 DAO 文件 completed
id content status
add-like-collect-goods-dao 新增 like.go、collect.go、goods.go 三个 DAO 文件 completed
id content status dependencies
add-order-quota-dao 新增 order.go、quota.go 两个 DAO 文件 completed
fix-user-dao
id content status
add-horoscope-notification-audit-vip-dao 新增 horoscope.go、notification.go、auditlog.go、vipflowlog.go 四个 DAO 文件 completed
id content status dependencies
verify-build 运行 go build、go vet 与 lint 验证编译通过 completed
fix-user-dao
add-clothes-outfit-closet-dao
add-like-collect-goods-dao
add-order-quota-dao
add-horoscope-notification-audit-vip-dao

产品概述

根据现有业务实体(internal/mysqlmodel)与接口 DTO(internal/dto)推断出的业务逻辑,补齐 internal/mysqldao 数据访问层,为后续 service 层提供统一、可测试、可替换的 GORM DAO。

核心功能

  • 覆盖全部业务实体:为 User、Clothes、Outfit/OutfitItem、ClosetShare/ClosetShareItem、Like、Collect、Goods、Order/RefundFlowLog、Quota/UserDailyUsage/QuotaFlowLog、Horoscope/AI 流水日志、UserNotification、审核日志、VIPFlowLog 提供 DAO。
  • 统一 CRUD 与列表:主键/UUID/业务编码查询、创建、条件更新、软删除,以及带总数返回的分页列表。
  • 业务一致性能力:Like/Collect 幂等(查记录→更新或创建)、订单/退款回调条件更新防重、配额原子扣减与回退、每日使用计数原子自增、Horoscope 与 AI 流水的幂等查询。
  • 风格对齐:沿用现有 struct{db *gorm.DB} + NewXxxDao() 模式,并将方法体统一改为 d.db.WithContext(ctx)(顺带修正 user.go 中直接使用 global.MysqlEngine 的写法)。

约束与边界

  • 仅修改/新增 internal/mysqldao/ 内文件,不实现 service、handler、routers。
  • DAO 不引入 app/errcode,错误统一返回 error(含 gorm.ErrRecordNotFound),由上层转换。
  • 全部使用 GORM 参数绑定防 SQL 注入,不拼接 SQL 字符串。

技术栈

  • 语言:Go
  • ORM:GORM v2(复用 global.MysqlEngine *gorm.DB)
  • 数据模型:internal/mysqlmodel(Model 基类含 ID int64 主键、CreatedAt/UpdatedAt int64、DeletedAt soft_delete.DeletedAt 软删除)

实现方案

DAO 组织方式

沿用现有 user.go 模式:每个 DAO 为 struct{ db *gorm.DB },提供 NewXxxDao() 构造函数注入 global.MysqlEngine,方法统一使用 d.db.WithContext(ctx)。一个文件承载一个或多个强相关 DAO(如 Outfit 与 OutfitItem 同文件)。

关键技术决策

  1. 统一 d.db:修正 user.go 直接调用 global.MysqlEngine 的写法,使 DAO 可替换、可测试。
  2. 软删除:依赖 Model.DeletedAt soft_delete.DeletedAt,gorm 查询自动过滤已删除记录;删除统一走 db.Delete(&model) 软删,不硬删。
  3. 分页 List 约定:DAO List 方法签名统一为 (ctx, 筛选条件..., page, pageSize int) (list, total int64, err error),内部先 Count(&total) 再 Offset((page-1)*pageSize).Limit(pageSize).Order("id DESC")。
  4. Like/Collect 幂等:目标表 TargetID 为 int64 主键(UUID→ID 转换在 service 层);DAO 提供 GetByTargetAndUser 供上层判断存在则 UpdateStatus,不存在则 Create。
  5. 订单/退款回调幂等:提供条件更新 UpdateStatusByOrderNo(orderNo, fromStatus, toStatus),用 WHERE order_no = ? AND order_status = ?,以 RowsAffected == 0 判定重复回调或状态非法。
  6. 配额原子扣减:IncreaseUsed 用 Where("user_id = ? AND quota_type = ? AND used + ? <= total", ...).UpdateColumn("used", gorm.Expr("used + ?", n)),以 RowsAffected 判断是否超额;DecreaseUsed 用 Where("used >= ?", n) 递减回退。
  7. 每日使用计数原子自增:UserDailyUsage 按 user_id + stat_date 唯一,提供按日期查询 + 对应字段 UpdateColumn(..., gorm.Expr("... + 1")) 自增。
  8. 错误处理:查询未命中返回 gorm.ErrRecordNotFound(First 直接返回),不吞错;Create/Update 直接透传 .Error。

目录结构

本次共涉及 13 个文件(1 个修改 + 12 个新增),全部位于 internal/mysqldao/:

internal/mysqldao/
├── user.go          # [MODIFY] UserDao 方法体统一 d.db;删除 UserQuotaListByUserID(迁移至 QuotaDao)
├── clothes.go       # [NEW] ClothesDao
├── outfit.go        # [NEW] OutfitDao + OutfitItemDao
├── closet.go        # [NEW] ClosetShareDao + ClosetShareItemDao
├── like.go          # [NEW] LikeDao
├── collect.go       # [NEW] CollectDao
├── goods.go         # [NEW] GoodsDao
├── order.go         # [NEW] OrderDao + RefundFlowLogDao
├── quota.go         # [NEW] QuotaDao + UserDailyUsageDao + QuotaFlowLogDao
├── horoscope.go     # [NEW] HoroscopeDao + AIHoroscopeFlowLogDao + AIRecommendFlowLogDao
├── notification.go  # [NEW] NotificationDao
├── auditlog.go      # [NEW] ClosetAuditLogDao + OutfitAuditLogDao
└── vipflowlog.go    # [NEW] VIPFlowLogDao

各 DAO 方法清单

ClothesDao(clothes.go)

  • ClothesCreate(ctx, *Clothes) error
  • ClothesGetByUUID(ctx, uuid string) (*Clothes, error)
  • ClothesGetByID(ctx, id int64) (*Clothes, error)
  • ClothesGetByUUIDs(ctx, uuids []string) ([]Clothes, error)
  • ClothesGetByIDs(ctx, ids []int64) ([]Clothes, error)
  • ClothesListByUserID(ctx, userID int64, categoryMain, categorySub string, page, pageSize int) ([]Clothes, int64, error)(大类/小类为空则不筛选)
  • ClothesUpdateByUUID(ctx, uuid string, updates map[string]any) error
  • ClothesDeleteByUUID(ctx, uuid string) error(软删)

OutfitDao + OutfitItemDao(outfit.go)

  • OutfitCreate/GetByUUID/GetByID/GetByUUIDs
  • OutfitListByUserID(ctx, userID int64, isPublic, source int8, page, pageSize int) ([]Outfit, int64, error)(isPublic/source 为 0 时不筛选)
  • OutfitUpdateByUUID(ctx, uuid string, updates map[string]any) error
  • OutfitUpdateIsPublic(ctx, uuid string, isPublic int8) error
  • OutfitDeleteByUUID(ctx, uuid string) error
  • OutfitItemCreateBatch(ctx, items []OutfitItem) error
  • OutfitItemListByOutfitID(ctx, outfitID int64) ([]OutfitItem, error)
  • OutfitItemListByOutfitIDs(ctx, outfitIDs []int64) ([]OutfitItem, error)
  • OutfitItemDeleteByOutfitID(ctx, outfitID int64) error

ClosetShareDao + ClosetShareItemDao(closet.go)

  • ClosetShareCreate/GetByUUID
  • ClosetShareListByUserID(ctx, userID int64, status int8, page, pageSize int) ([]ClosetShare, int64, error)
  • ClosetShareUpdateStatus(ctx, shareUUID string, status int8) error(取消/封禁)
  • ClosetShareItemCreateBatch/ListByShareUUID/ListByShareUUIDs/DeleteByShareUUID

LikeDao(like.go)

  • LikeCreate(ctx, *Like) error
  • LikeGetByTargetAndUser(ctx, likeType int8, targetID, userID int64) (*Like, error)
  • LikeUpdateStatus(ctx, id int64, status int8) error
  • LikeCountByTarget(ctx, likeType int8, targetID int64) (int64, error)
  • LikeListUserIDsByTarget(ctx, likeType int8, targetID int64) ([]int64, error)

CollectDao(collect.go)

  • CollectCreate/GetByTargetAndUser/UpdateStatus
  • CollectCountByTarget(ctx, collectType int8, targetID int64) (int64, error)
  • CollectListTargetIDsByUser(ctx, collectType int8, userID int64) ([]int64, error)

GoodsDao(goods.go)

  • GoodsGetByCode(ctx, code string) (*Goods, error)
  • GoodsGetByID(ctx, id int64) (*Goods, error)
  • GoodsList(ctx, goodsType int8, page, pageSize int) ([]Goods, int64, error)(仅 status = 1 上架;goodsType 为 0 时不筛选)

OrderDao + RefundFlowLogDao(order.go)

  • OrderCreate/GetByID/GetByOrderNo
  • OrderListByUserID(ctx, userID int64, status int8, page, pageSize int) ([]Order, int64, error)
  • OrderUpdateByOrderNo(ctx, orderNo string, updates map[string]any) error
  • OrderUpdateStatusByOrderNo(ctx, orderNo string, fromStatus, toStatus int8) (bool, error)(条件更新,RowsAffected 判重)
  • RefundFlowLogCreate/GetByRefundNo/GetByOrderNo
  • RefundFlowLogUpdateStatus(ctx, refundNo string, updates map[string]any) error

QuotaDao + UserDailyUsageDao + QuotaFlowLogDao(quota.go)

  • QuotaListByUserID(ctx, userID int64) ([]Quota, error)(自 user.go 迁移)
  • QuotaGetByUserIDAndType(ctx, userID int64, quotaType int8) (*Quota, error)
  • QuotaCreate(ctx, *Quota) error
  • QuotaIncreaseUsed(ctx, userID int64, quotaType int8, n int64) (bool, error)(原子扣减,超额返回 false)
  • QuotaDecreaseUsed(ctx, userID int64, quotaType int8, n int64) error(回退)
  • UserDailyUsageGetByUserIDAndDate/Create
  • UserDailyUsageIncrementAI/IncrementOutfitShare/IncrementClosetShare
  • QuotaFlowLogCreate/ListByUserID(userID, quotaType, page, pageSize)

HoroscopeDao + AI 流水(horoscope.go)

  • HoroscopeGetByZodiacAndDate(ctx, zodiac string, statDate int64) (*Horoscope, error)
  • HoroscopeCreate(ctx, *Horoscope) error
  • AIHoroscopeFlowLogGetByRequestID/Create
  • AIRecommendFlowLogGetByRequestID/Create

NotificationDao(notification.go)

  • NotificationCreate/GetByUUID
  • NotificationListByUserID(ctx, userID int64, isRead int8, page, pageSize int) ([]UserNotification, int64, error)
  • NotificationUpdateReadByUUID(ctx, uuid string) error
  • NotificationCountUnread(ctx, userID int64) (int64, error)

审核日志(auditlog.go)

  • ClosetAuditLogCreate/ListByTargetID
  • OutfitAuditLogCreate/ListByTargetID

VIPFlowLogDao(vipflowlog.go)

  • VIPFlowLogCreate
  • VIPFlowLogListByUserID(ctx, userID int64, page, pageSize int) ([]VIPFlowLog, int64, error)

关键代码结构

DAO 构造与查询统一模式

type XxxDao struct{ db *gorm.DB }

func NewXxxDao() *XxxDao { return &XxxDao{db: global.MysqlEngine} }

func (d *XxxDao) XxxGetByID(ctx context.Context, id int64) (*mysqlmodel.Xxx, error) {
    var m mysqlmodel.Xxx
    if err := d.db.WithContext(ctx).First(&m, id).Error; err != nil {
        return nil, err
    }
    return &m, nil
}

分页 List 模式

func (d *XxxDao) XxxList(ctx context.Context, page, pageSize int) ([]mysqlmodel.Xxx, int64, error) {
    var list []mysqlmodel.Xxx
    var total int64
    q := d.db.WithContext(ctx).Model(&mysqlmodel.Xxx{})
    if err := q.Count(&total).Error; err != nil {
        return nil, 0, err
    }
    err := q.Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&list).Error
    return list, total, err
}

原子扣减 / 条件更新模式

// 配额原子扣减:超额时 RowsAffected 为 0
res := d.db.WithContext(ctx).Model(&mysqlmodel.Quota{}).
    Where("user_id = ? AND quota_type = ? AND used + ? <= total", userID, quotaType, n).
    UpdateColumn("used", gorm.Expr("used + ?", n))
if res.Error != nil { return false, res.Error }
return res.RowsAffected > 0, nil

// 订单状态条件流转:防重复回调
res := d.db.WithContext(ctx).Model(&mysqlmodel.Order{}).
    Where("order_no = ? AND order_status = ?", orderNo, fromStatus).
    Update("order_status", toStatus)
return res.RowsAffected > 0, res.Error

实施要点

  • 影响范围可控:仅新增/修改 internal/mysqldao/,不触碰 mysqlmodel、dto、redisdao 及其他模块;对外仅移除 user.go 的 UserQuotaListByUserID(当前无调用方)。
  • 性能:列表查询采用先 Count 再分页的单次复用 Query 链;批量查询 items 使用 WHERE id IN (?) 一次取回,避免 N+1;无额外开销。
  • 安全:所有条件使用 ? 占位符绑定,不拼接 SQL;软删除交由 gorm 处理。
  • 兼容性:gorm.ErrRecordNotFound 原样返回,供 service 层用 errors.Is 判断;本次不引入新依赖。