2026-06-26 23:21:22 +08:00

151 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package mysql 实现 room-service 的 MySQL 持久化边界。
package mysql
import (
"context"
"database/sql"
"errors"
"sort"
"strings"
"time"
mysqlerr "github.com/go-sql-driver/mysql"
"hyapp/pkg/appcode"
"hyapp/pkg/xerr"
)
const (
roomOutboxTable = "room_outbox"
)
// Repository 把 room-service 的权威状态持久化到 MySQL。
type Repository struct {
// db 是标准库连接池,所有 repository 方法都通过它访问 MySQL。
db *sql.DB
}
// Open 建立 MySQL 连接池,并验证数据库可用。
func Open(ctx context.Context, dsn string, maxOpenConns int, maxIdleConns int) (*Repository, error) {
// sql.Open 只校验 driver 和 DSN 形态,真实连通性由后续 PingContext 验证。
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, err
}
if maxOpenConns > 0 {
// 只在配置显式给出正数时覆盖标准库默认值。
db.SetMaxOpenConns(maxOpenConns)
}
if maxIdleConns > 0 {
// 空闲连接数需要小于等于最大连接数,由配置层负责合理设置。
db.SetMaxIdleConns(maxIdleConns)
}
// 长连接定期轮换,避免云数据库或网络层回收导致长期坏连接驻留。
db.SetConnMaxLifetime(30 * time.Minute)
if err := db.PingContext(ctx); err != nil {
// 启动探测失败时必须关闭连接池,避免返回半可用 repository。
_ = db.Close()
return nil, err
}
return &Repository{db: db}, nil
}
// Close 关闭底层 MySQL 连接池。
func (r *Repository) Close() error {
// App.Close 调用该方法释放所有空闲连接,并阻止后续新查询。
return r.db.Close()
}
// Ping 验证 MySQL 连接池当前仍能在调用方 deadline 内拿到可用连接。
func (r *Repository) Ping(ctx context.Context) error {
// ready 探针通过该方法确认 command log/snapshot/outbox 的持久化来源可用。
return r.db.PingContext(ctx)
}
const (
roomPresenceStatusActiveSQL = "active"
roomPresenceStatusLeftSQL = "left"
roomFollowStatusActiveSQL = "active"
roomFollowStatusCancelledSQL = "cancelled"
)
func escapeRoomListLike(value string) string {
var builder strings.Builder
for _, r := range value {
if r == '\\' || r == '%' || r == '_' {
builder.WriteRune('\\')
}
builder.WriteRune(r)
}
return builder.String()
}
func nullableString(value string) sql.Null[string] {
// 空错误写 NULL便于 SQL 查询区分没有错误和空字符串。
return sql.Null[string]{
V: value,
Valid: value != "",
}
}
func normalizedRecordAppCode(ctx context.Context, value string) string {
value = strings.TrimSpace(value)
if value == "" {
return appcode.FromContext(ctx)
}
return appcode.Normalize(value)
}
func boolToInt(value bool) int {
if value {
return 1
}
return 0
}
func firstPositiveInt64(values ...int64) int64 {
for _, value := range values {
if value > 0 {
return value
}
}
return 0
}
func uniquePositiveInt64(values []int64) []int64 {
seen := make(map[int64]bool, len(values))
out := make([]int64, 0, len(values))
for _, value := range values {
if value <= 0 || seen[value] {
continue
}
seen[value] = true
out = append(out, value)
}
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
return out
}
func mapRoomMetaDuplicateError(err error) error {
if err == nil {
return nil
}
var mysqlErr *mysqlerr.MySQLError
if !errors.As(err, &mysqlErr) || mysqlErr.Number != 1062 {
return err
}
message := mysqlErr.Message
if strings.Contains(message, "uk_rooms_owner_user") || strings.Contains(message, "owner_user_id") {
return xerr.New(xerr.Conflict, "owner already has room")
}
return xerr.New(xerr.Conflict, "room already exists")
}