wip: 登录和认证

This commit is contained in:
loveuer
2025-07-13 22:57:57 +08:00
parent 48af538f98
commit b48fa05d9f
33 changed files with 1961 additions and 33 deletions

98
pkg/database/cache/cache.go vendored Normal file
View File

@ -0,0 +1,98 @@
package cache
import (
"context"
"encoding/json"
"errors"
"sync"
"time"
)
type encoded_value interface {
MarshalBinary() ([]byte, error)
}
type decoded_value interface {
UnmarshalBinary(bs []byte) error
}
type Scanner interface {
Scan(model any) error
}
type scan struct {
err error
bs []byte
}
func newScan(bs []byte, err error) *scan {
return &scan{bs: bs, err: err}
}
func (s *scan) Scan(model any) error {
if s.err != nil {
return s.err
}
return unmarshaler(s.bs, model)
}
type Cache interface {
Get(ctx context.Context, key string) ([]byte, error)
Gets(ctx context.Context, keys ...string) ([][]byte, error)
GetScan(ctx context.Context, key string) Scanner
GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error)
GetExScan(ctx context.Context, key string, duration time.Duration) Scanner
// Set value 会被序列化, 优先使用 MarshalBinary 方法, 没有则执行 json.Marshal
Set(ctx context.Context, key string, value any) error
Sets(ctx context.Context, vm map[string]any) error
// SetEx value 会被序列化, 优先使用 MarshalBinary 方法, 没有则执行 json.Marshal
SetEx(ctx context.Context, key string, value any, duration time.Duration) error
Del(ctx context.Context, keys ...string) error
GetDel(ctx context.Context, key string) ([]byte, error)
GetDelScan(ctx context.Context, key string) Scanner
Close()
Client() any
}
var (
lock = &sync.Mutex{}
marshaler func(data any) ([]byte, error) = json.Marshal
unmarshaler func(data []byte, model any) error = json.Unmarshal
ErrorKeyNotFound = errors.New("key not found")
ErrorStoreFailed = errors.New("store failed")
Default Cache
)
func handleValue(value any) ([]byte, error) {
var (
bs []byte
err error
)
switch val := value.(type) {
case []byte:
return val, nil
}
if imp, ok := value.(encoded_value); ok {
bs, err = imp.MarshalBinary()
} else {
bs, err = marshaler(value)
}
return bs, err
}
func SetMarshaler(fn func(data any) ([]byte, error)) {
lock.Lock()
defer lock.Unlock()
marshaler = fn
}
func SetUnmarshaler(fn func(data []byte, model any) error) {
lock.Lock()
defer lock.Unlock()
unmarshaler = fn
}

155
pkg/database/cache/memory.go vendored Normal file
View File

@ -0,0 +1,155 @@
package cache
import (
"context"
"errors"
"time"
"github.com/dgraph-io/ristretto/v2"
)
var _ Cache = (*_mem)(nil)
type _mem struct {
ctx context.Context
cache *ristretto.Cache[string, []byte]
}
func newMemory(ctx context.Context, ins *ristretto.Cache[string, []byte]) Cache {
return &_mem{
ctx: ctx,
cache: ins,
}
}
func (m *_mem) Client() any {
return m.cache
}
func (c *_mem) Close() {
c.cache.Close()
}
func (c *_mem) Del(ctx context.Context, keys ...string) error {
for _, key := range keys {
c.cache.Del(key)
}
return nil
}
func (c *_mem) Get(ctx context.Context, key string) ([]byte, error) {
val, ok := c.cache.Get(key)
if !ok {
return val, ErrorKeyNotFound
}
return val, nil
}
func (c *_mem) GetDel(ctx context.Context, key string) ([]byte, error) {
val, err := c.Get(ctx, key)
if err != nil {
return val, err
}
c.cache.Del(key)
return val, err
}
func (c *_mem) GetDelScan(ctx context.Context, key string) Scanner {
val, err := c.GetDel(ctx, key)
return newScan(val, err)
}
func (c *_mem) GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) {
val, err := c.Get(ctx, key)
if err != nil {
return val, err
}
c.cache.SetWithTTL(key, val, 1, duration)
return val, err
}
func (m *_mem) GetExScan(ctx context.Context, key string, duration time.Duration) Scanner {
val, err := m.GetEx(ctx, key, duration)
return newScan(val, err)
}
func (m *_mem) GetScan(ctx context.Context, key string) Scanner {
val, err := m.Get(ctx, key)
return newScan(val, err)
}
func (m *_mem) Gets(ctx context.Context, keys ...string) ([][]byte, error) {
vals := make([][]byte, 0, len(keys))
for _, key := range keys {
val, err := m.Get(ctx, key)
if err != nil {
if errors.Is(err, ErrorKeyNotFound) {
continue
}
return vals, err
}
vals = append(vals, val)
}
if len(vals) != len(keys) {
return vals, ErrorKeyNotFound
}
return vals, nil
}
func (m *_mem) Set(ctx context.Context, key string, value any) error {
val, err := handleValue(value)
if err != nil {
return err
}
if ok := m.cache.Set(key, val, 1); !ok {
return ErrorStoreFailed
}
m.cache.Wait()
return nil
}
func (m *_mem) SetEx(ctx context.Context, key string, value any, duration time.Duration) error {
val, err := handleValue(value)
if err != nil {
return err
}
if ok := m.cache.SetWithTTL(key, val, 1, duration); !ok {
return ErrorStoreFailed
}
m.cache.Wait()
return nil
}
func (m *_mem) Sets(ctx context.Context, vm map[string]any) error {
for key, value := range vm {
val, err := handleValue(value)
if err != nil {
return err
}
if ok := m.cache.Set(key, val, 1); !ok {
return ErrorStoreFailed
}
}
m.cache.Wait()
return nil
}

84
pkg/database/cache/new.go vendored Normal file
View File

@ -0,0 +1,84 @@
package cache
import (
"context"
"fmt"
"net/url"
"gitea.loveuer.com/yizhisec/packages/tool"
"github.com/dgraph-io/ristretto/v2"
"github.com/go-redis/redis/v8"
)
func New(opts ...Option) (Cache, error) {
var (
err error
cfg = &option{
ctx: context.Background(),
}
)
for _, opt := range opts {
opt(cfg)
}
if cfg.redis != nil {
var (
ins *url.URL
client *redis.Client
)
if ins, err = url.Parse(*cfg.redis); err != nil {
return nil, err
}
username := ins.User.Username()
password, _ := ins.User.Password()
client = redis.NewClient(&redis.Options{
Addr: ins.Host,
Username: username,
Password: password,
})
if err = client.Ping(tool.CtxTimeout(cfg.ctx, 5)).Err(); err != nil {
return nil, err
}
return newRedis(cfg.ctx, client), nil
}
if cfg.memory {
var (
ins *ristretto.Cache[string, []byte]
)
if ins, err = ristretto.NewCache(&ristretto.Config[string, []byte]{
NumCounters: 1e7, // number of keys to track frequency of (10M).
MaxCost: 1 << 30, // maximum cost of cache (1GB).
BufferItems: 64, // number of keys per Get buffer.
}); err != nil {
return nil, err
}
return newMemory(cfg.ctx, ins), nil
}
return nil, fmt.Errorf("invalid cache option")
}
func Init(opts ...Option) (err error) {
opt := &option{}
for _, optFn := range opts {
optFn(opt)
}
if opt.memory {
Default, err = New(opts...)
return err
}
Default, err = New(opts...)
return err
}

108
pkg/database/cache/new_test.go vendored Normal file
View File

@ -0,0 +1,108 @@
package cache
import (
"testing"
)
func TestNew(t *testing.T) {
/* if err := Init(WithRedis("127.0.0.1", 6379, "", "MyPassw0rd")); err != nil {
t.Fatal(err)
}
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
if err := Default.Set(t.Context(), "zyp:haha", &User{
Name: "cache",
Age: 18,
}); err != nil {
t.Fatal(err)
}
s := Default.GetDelScan(t.Context(), "zyp:haha")
u := new(User)
if err := s.Scan(u); err != nil {
t.Fatal(err)
}
t.Logf("%#v", *u)
if err := Default.SetEx(t.Context(), "zyp:haha", &User{
Name: "redis",
Age: 2,
}, time.Hour); err != nil {
t.Fatal(err)
}*/
}
func TestNoAuth(t *testing.T) {
//if err := Init(WithRedis("10.125.1.28", 6379, "", "")); err != nil {
// t.Fatal(err)
//}
//
//type User struct {
// Name string `json:"name"`
// Age int `json:"age"`
//}
//
//if err := Default.Set(t.Context(), "zyp:haha", &User{
// Name: "cache",
// Age: 18,
//}); err != nil {
// t.Fatal(err)
//}
//
//s := Default.GetDelScan(t.Context(), "zyp:haha")
//u := new(User)
//
//if err := s.Scan(u); err != nil {
// t.Fatal(err)
//}
//
//t.Logf("%#v", *u)
//
//if err := Default.SetEx(t.Context(), "zyp:haha", &User{
// Name: "redis",
// Age: 2,
//}, time.Hour); err != nil {
// t.Fatal(err)
//}
}
func TestMemoryDefault(t *testing.T) {
if err := Init(WithMemory()); err != nil {
t.Fatal("init err:", err)
}
if err := Default.Set(t.Context(), "123", "123"); err != nil {
t.Fatal("set err:", err)
}
val, err := Default.Get(t.Context(), "123")
if err != nil {
t.Fatal("get err:", err)
}
t.Logf("%s", val)
}
func TestMemoryNew(t *testing.T) {
client, err := New(WithMemory())
if err != nil {
t.Fatal("init err:", err)
}
if err := client.Set(t.Context(), "123", "123"); err != nil {
t.Fatal("set err:", err)
}
val, err := client.Get(t.Context(), "123")
if err != nil {
t.Fatal("get err:", err)
}
t.Logf("%s", val)
}

55
pkg/database/cache/option.go vendored Normal file
View File

@ -0,0 +1,55 @@
package cache
import (
"context"
"fmt"
"net/url"
)
type option struct {
ctx context.Context
redis *string
memory bool
}
type Option func(*option)
func WithCtx(ctx context.Context) Option {
return func(c *option) {
if ctx != nil {
c.ctx = ctx
}
}
}
func WithRedis(host string, port int, username, password string) Option {
return func(c *option) {
uri := fmt.Sprintf("redis://%s:%d", host, port)
if username != "" || password != "" {
uri = fmt.Sprintf("redis://%s:%s@%s:%d", username, password, host, port)
}
c.redis = &uri
}
}
func WithRedisURI(uri string) Option {
return func(c *option) {
ins, err := url.Parse(uri)
if err != nil {
return
}
if ins.Scheme != "redis" {
return
}
c.redis = &uri
}
}
func WithMemory() Option {
return func(c *option) {
c.memory = true
}
}

153
pkg/database/cache/redis.go vendored Normal file
View File

@ -0,0 +1,153 @@
package cache
import (
"context"
"errors"
"sync"
"time"
"gitea.loveuer.com/yizhisec/packages/tool"
"github.com/go-redis/redis/v8"
"github.com/spf13/cast"
)
var _ Cache = (*_redis)(nil)
type _redis struct {
sync.Mutex
ctx context.Context
client *redis.Client
}
func (r *_redis) Client() any {
return r.client
}
func newRedis(ctx context.Context, client *redis.Client) Cache {
r := &_redis{ctx: ctx, client: client}
go func() {
<-r.ctx.Done()
if client != nil {
r.Close()
}
}()
return r
}
func (r *_redis) GetDel(ctx context.Context, key string) ([]byte, error) {
s, err := r.client.GetDel(ctx, key).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, ErrorKeyNotFound
}
return nil, err
}
return tool.StringToBytes(s), nil
}
func (r *_redis) GetDelScan(ctx context.Context, key string) Scanner {
bs, err := r.GetDel(ctx, key)
return newScan(bs, err)
}
func (r *_redis) Get(ctx context.Context, key string) ([]byte, error) {
result, err := r.client.Get(ctx, key).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, ErrorKeyNotFound
}
return nil, err
}
return tool.StringToBytes(result), nil
}
func (r *_redis) Gets(ctx context.Context, keys ...string) ([][]byte, error) {
result, err := r.client.MGet(ctx, keys...).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, ErrorKeyNotFound
}
return nil, err
}
return tool.Map(
result,
func(item any, index int) []byte {
return tool.StringToBytes(cast.ToString(item))
},
), nil
}
func (r *_redis) GetScan(ctx context.Context, key string) Scanner {
return newScan(r.Get(ctx, key))
}
func (r *_redis) GetEx(ctx context.Context, key string, duration time.Duration) ([]byte, error) {
result, err := r.client.GetEx(ctx, key, duration).Result()
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, ErrorKeyNotFound
}
return nil, err
}
return tool.StringToBytes(result), nil
}
func (r *_redis) GetExScan(ctx context.Context, key string, duration time.Duration) Scanner {
return newScan(r.GetEx(ctx, key, duration))
}
func (r *_redis) Set(ctx context.Context, key string, value any) error {
bs, err := handleValue(value)
if err != nil {
return err
}
_, err = r.client.Set(ctx, key, bs, redis.KeepTTL).Result()
return err
}
func (r *_redis) Sets(ctx context.Context, values map[string]any) error {
vm := make(map[string]any)
for k, v := range values {
bs, err := handleValue(v)
if err != nil {
return err
}
vm[k] = bs
}
return r.client.MSet(ctx, vm).Err()
}
func (r *_redis) SetEx(ctx context.Context, key string, value any, duration time.Duration) error {
bs, err := handleValue(value)
if err != nil {
return err
}
_, err = r.client.SetEX(ctx, key, bs, duration).Result()
return err
}
func (r *_redis) Del(ctx context.Context, keys ...string) error {
return r.client.Del(ctx, keys...).Err()
}
func (r *_redis) Close() {
r.Lock()
defer r.Unlock()
_ = r.client.Close()
r.client = nil
}