package shop import ( "context" stderrors "errors" "reflect" "github.com/jackc/pgx/v5/pgconn" "gorm.io/gorm" distributiondomain "github.com/break/junhong_cmp_fiber/internal/domain/distribution" "github.com/break/junhong_cmp_fiber/internal/model" "github.com/break/junhong_cmp_fiber/pkg/constants" "github.com/break/junhong_cmp_fiber/pkg/errors" ) // distributionCodeConstraint 是分销码条件唯一索引名,用于识别唯一冲突并重试。 const distributionCodeConstraint = "uk_shop_distribution_code" // distributionCodeSavepoint 是分销码冲突重试使用的保存点名称。 const distributionCodeSavepoint = "shop_distribution_code_retry" // CreateShopWithDistributionCode 在事务内为新店铺生成全局唯一随机分销码并创建店铺。 // // 每次尝试都重新生成随机码;Create 命中分销码唯一约束时重新生成并重试, // 最多 constants.ShopDistributionCodeMaxAttempts 次。其他唯一冲突(店铺编号等)不重试, // 直接返回数据库错误。 // // 重试依赖真实保存点:每条 Create 包在 GORM 的嵌套事务中执行,冲突时 GORM 自动 // 回滚到内部保存点,外层事务因此仍可用(PostgreSQL 唯一冲突会中止整个事务, // 不回滚到保存点则后续语句必然 25P02,重试不可能生效)。这里刻意不使用裸 // SavePoint/RollbackTo:GORM 的嵌套事务会自行处理 PrepareStmt 下的连接池切换。 func CreateShopWithDistributionCode(ctx context.Context, tx *gorm.DB, shop *model.Shop) error { if tx == nil || shop == nil { return errors.New(errors.CodeInvalidParam, "创建店铺参数无效") } // 失败关闭:必须在调用方的事务句柄内执行,否则嵌套事务会自行开启并提交一个新事务, // 破坏调用方的原子性(建店事务与注册审批通过事务均满足该前提)。 if !inTransaction(tx) { return errors.New(errors.CodeInvalidStatus, "创建店铺必须传入事务句柄") } for range constants.ShopDistributionCodeMaxAttempts { code, err := distributiondomain.GenerateDistributionCode() if err != nil { return err } if occupied, err := distributionCodeOccupied(ctx, tx, code); err != nil { return err } else if occupied { // 预检命中直接换码,避免把可预期的冲突交给数据库。 continue } shop.DistributionCode = code shop.ID = 0 createErr := tx.WithContext(ctx).Transaction(func(inner *gorm.DB) error { return inner.Create(shop).Error }) if createErr == nil { return nil } if !isDistributionCodeConflict(createErr) { return errors.Wrap(errors.CodeDatabaseError, createErr, "创建店铺失败") } // 分销码冲突:GORM 已回滚到内部保存点,外层事务仍可继续,换码重试。 } return errors.New(errors.CodeConflict, "生成分销码冲突,请重试") } // inTransaction 判断句柄是否为已开启的事务,与 GORM 自身识别嵌套事务的方式一致。 func inTransaction(tx *gorm.DB) bool { if tx == nil || tx.Statement == nil { return false } committer, ok := tx.Statement.ConnPool.(gorm.TxCommitter) return ok && committer != nil && !reflect.ValueOf(committer).IsNil() } // distributionCodeOccupied 预检分销码是否已被未删除店铺占用。 func distributionCodeOccupied(ctx context.Context, tx *gorm.DB, code string) (bool, error) { var count int64 if err := tx.WithContext(ctx).Model(&model.Shop{}). Where("distribution_code = ?", code).Count(&count).Error; err != nil { return false, errors.Wrap(errors.CodeDatabaseError, err, "校验分销码唯一性失败") } return count > 0, nil } // isDistributionCodeConflict 判断错误是否为分销码条件唯一索引冲突。 func isDistributionCodeConflict(err error) bool { var pgErr *pgconn.PgError if !stderrors.As(err, &pgErr) { return false } return pgErr.Code == "23505" && pgErr.ConstraintName == distributionCodeConstraint }