12 KiB
12 KiB
name, overview, todos
| name | overview | todos | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 补齐 mysqldao 数据访问层 | 根据 mysqlmodel 全部实体与 dto 暴露的业务接口,补齐 internal/mysqldao 下所有 DAO(单表 CRUD + 常用查询,含分页、软删除、原子扣减、幂等),沿用现有 user.go 风格。 |
|
产品概述
根据现有业务实体(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 同文件)。
关键技术决策
- 统一
d.db:修正 user.go 直接调用global.MysqlEngine的写法,使 DAO 可替换、可测试。 - 软删除:依赖
Model.DeletedAt soft_delete.DeletedAt,gorm 查询自动过滤已删除记录;删除统一走db.Delete(&model)软删,不硬删。 - 分页 List 约定:DAO List 方法签名统一为
(ctx, 筛选条件..., page, pageSize int) (list, total int64, err error),内部先Count(&total)再Offset((page-1)*pageSize).Limit(pageSize).Order("id DESC")。 - Like/Collect 幂等:目标表
TargetID为 int64 主键(UUID→ID 转换在 service 层);DAO 提供GetByTargetAndUser供上层判断存在则UpdateStatus,不存在则Create。 - 订单/退款回调幂等:提供条件更新
UpdateStatusByOrderNo(orderNo, fromStatus, toStatus),用WHERE order_no = ? AND order_status = ?,以RowsAffected == 0判定重复回调或状态非法。 - 配额原子扣减:
IncreaseUsed用Where("user_id = ? AND quota_type = ? AND used + ? <= total", ...).UpdateColumn("used", gorm.Expr("used + ?", n)),以 RowsAffected 判断是否超额;DecreaseUsed用Where("used >= ?", n)递减回退。 - 每日使用计数原子自增:
UserDailyUsage按user_id + stat_date唯一,提供按日期查询 + 对应字段UpdateColumn(..., gorm.Expr("... + 1"))自增。 - 错误处理:查询未命中返回
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) errorClothesGetByUUID(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) errorClothesDeleteByUUID(ctx, uuid string) error(软删)
OutfitDao + OutfitItemDao(outfit.go)
OutfitCreate/GetByUUID/GetByID/GetByUUIDsOutfitListByUserID(ctx, userID int64, isPublic, source int8, page, pageSize int) ([]Outfit, int64, error)(isPublic/source 为 0 时不筛选)OutfitUpdateByUUID(ctx, uuid string, updates map[string]any) errorOutfitUpdateIsPublic(ctx, uuid string, isPublic int8) errorOutfitDeleteByUUID(ctx, uuid string) errorOutfitItemCreateBatch(ctx, items []OutfitItem) errorOutfitItemListByOutfitID(ctx, outfitID int64) ([]OutfitItem, error)OutfitItemListByOutfitIDs(ctx, outfitIDs []int64) ([]OutfitItem, error)OutfitItemDeleteByOutfitID(ctx, outfitID int64) error
ClosetShareDao + ClosetShareItemDao(closet.go)
ClosetShareCreate/GetByUUIDClosetShareListByUserID(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) errorLikeGetByTargetAndUser(ctx, likeType int8, targetID, userID int64) (*Like, error)LikeUpdateStatus(ctx, id int64, status int8) errorLikeCountByTarget(ctx, likeType int8, targetID int64) (int64, error)LikeListUserIDsByTarget(ctx, likeType int8, targetID int64) ([]int64, error)
CollectDao(collect.go)
CollectCreate/GetByTargetAndUser/UpdateStatusCollectCountByTarget(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/GetByOrderNoOrderListByUserID(ctx, userID int64, status int8, page, pageSize int) ([]Order, int64, error)OrderUpdateByOrderNo(ctx, orderNo string, updates map[string]any) errorOrderUpdateStatusByOrderNo(ctx, orderNo string, fromStatus, toStatus int8) (bool, error)(条件更新,RowsAffected 判重)RefundFlowLogCreate/GetByRefundNo/GetByOrderNoRefundFlowLogUpdateStatus(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) errorQuotaIncreaseUsed(ctx, userID int64, quotaType int8, n int64) (bool, error)(原子扣减,超额返回 false)QuotaDecreaseUsed(ctx, userID int64, quotaType int8, n int64) error(回退)UserDailyUsageGetByUserIDAndDate/CreateUserDailyUsageIncrementAI/IncrementOutfitShare/IncrementClosetShareQuotaFlowLogCreate/ListByUserID(userID, quotaType, page, pageSize)
HoroscopeDao + AI 流水(horoscope.go)
HoroscopeGetByZodiacAndDate(ctx, zodiac string, statDate int64) (*Horoscope, error)HoroscopeCreate(ctx, *Horoscope) errorAIHoroscopeFlowLogGetByRequestID/CreateAIRecommendFlowLogGetByRequestID/Create
NotificationDao(notification.go)
NotificationCreate/GetByUUIDNotificationListByUserID(ctx, userID int64, isRead int8, page, pageSize int) ([]UserNotification, int64, error)NotificationUpdateReadByUUID(ctx, uuid string) errorNotificationCountUnread(ctx, userID int64) (int64, error)
审核日志(auditlog.go)
ClosetAuditLogCreate/ListByTargetIDOutfitAuditLogCreate/ListByTargetID
VIPFlowLogDao(vipflowlog.go)
VIPFlowLogCreateVIPFlowLogListByUserID(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判断;本次不引入新依赖。