119 lines
3.4 KiB
Go
119 lines
3.4 KiB
Go
// Package mysql 实现 user-service 的 MySQL 持久化边界。
|
||
package mysql
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"errors"
|
||
"strings"
|
||
|
||
mysqldriver "github.com/go-sql-driver/mysql"
|
||
"hyapp/pkg/xerr"
|
||
)
|
||
|
||
// Repository 是 user-service 的 MySQL 持久化入口。
|
||
// 方法按领域拆到 user_repository、identity_repository 和 auth_repository,避免一个文件混合多条业务线。
|
||
type Repository struct {
|
||
// db 是标准库连接池,事务和普通查询都从这里创建。
|
||
db *sql.DB
|
||
}
|
||
|
||
// Open 创建 MySQL 连接池并做启动 ping。
|
||
func Open(ctx context.Context, dsn string) (*Repository, error) {
|
||
if strings.TrimSpace(dsn) == "" {
|
||
// 本地和线上运行都必须显式提供 DSN;空 DSN 代表应用装配错误。
|
||
return nil, xerr.New(xerr.InvalidArgument, "mysql_dsn is required")
|
||
}
|
||
|
||
// sql.Open 只创建连接池,真实连通性由 PingContext 验证。
|
||
db, err := sql.Open("mysql", dsn)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
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 {
|
||
if r == nil || r.db == nil {
|
||
// Close 允许重复调用和 nil 调用,方便 App.Close 幂等释放。
|
||
return nil
|
||
}
|
||
|
||
return r.db.Close()
|
||
}
|
||
|
||
// Ping 验证 MySQL 当前可用。
|
||
func (r *Repository) Ping(ctx context.Context) error {
|
||
if r == nil || r.db == nil {
|
||
// 没有连接池时明确返回 unavailable。
|
||
return xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||
}
|
||
|
||
return r.db.PingContext(ctx)
|
||
}
|
||
|
||
// mapDuplicateError 把通用 display_user_id 唯一冲突转换成稳定领域 reason。
|
||
func mapDuplicateError(err error) error {
|
||
var mysqlErr *mysqldriver.MySQLError
|
||
if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
|
||
// 默认短号冲突对 service 层使用稳定领域错误。
|
||
return xerr.New(xerr.DisplayUserIDExists, "display_user_id already exists")
|
||
}
|
||
|
||
return err
|
||
}
|
||
|
||
// mapPrettyDuplicateError 把靓号租约相关唯一冲突转换成靓号不可用。
|
||
func mapPrettyDuplicateError(err error) error {
|
||
var mysqlErr *mysqldriver.MySQLError
|
||
if errors.As(err, &mysqlErr) && mysqlErr.Number == 1062 {
|
||
// 靓号唯一约束冲突和默认短号冲突使用不同 reason。
|
||
return xerr.New(xerr.DisplayUserIDPrettyNotAvailable, "pretty display_user_id is not available")
|
||
}
|
||
|
||
return err
|
||
}
|
||
|
||
// nullableString 统一把空字符串写成 SQL NULL,避免空值参与唯一性或审计语义。
|
||
func nullableString(value string) any {
|
||
if strings.TrimSpace(value) == "" {
|
||
// 空字符串统一写 NULL,避免唯一索引或审计字段产生假值。
|
||
return nil
|
||
}
|
||
|
||
return value
|
||
}
|
||
|
||
// nullableOperator 让系统修改短号时可以不携带 operator_user_id。
|
||
func nullableOperator(operatorUserID int64) any {
|
||
if operatorUserID <= 0 {
|
||
// 系统任务或懒过期不一定有操作者。
|
||
return nil
|
||
}
|
||
|
||
return operatorUserID
|
||
}
|
||
|
||
// nullableRegionID 统一把无区域归属写成 SQL NULL,查询投影再转换回 0。
|
||
func nullableRegionID(regionID int64) any {
|
||
if regionID <= 0 {
|
||
return nil
|
||
}
|
||
|
||
return regionID
|
||
}
|
||
|
||
// queryer 抽象 *sql.DB 和 *sql.Tx 的 QueryRowContext,便于读方法复用事务锁定查询。
|
||
type queryer interface {
|
||
// QueryRowContext 是 *sql.DB 和 *sql.Tx 共同具备的方法。
|
||
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
|
||
}
|