feat: uniq tenant_users

This commit is contained in:
Rogee
2024-12-02 11:29:15 +08:00
parent d1a2a80859
commit 2b4cfb1a1e
7 changed files with 198 additions and 37 deletions

View File

@@ -4,16 +4,15 @@
CREATE TABLE
users (
id SERIAL8 PRIMARY KEY,
open_id VARCHAR(128) NOT NULL,
union_id VARCHAR(128),
oauth jsonb ,
open_id VARCHAR(128) NOT NULL UNIQUE,
union_id VARCHAR(128) ,
oauth jsonb default '{}'::jsonb,
expire_in timestamp NOT NULL,
created_at timestamp NOT NULL,
updated_at timestamp NOT NULL
created_at timestamp NOT NULL default now(),
updated_at timestamp NOT NULL default now()
);
CREATE INDEX idx_users_open_id ON users (open_id);
CREATE INDEX idx_users_union_id ON users (union_id);
-- table tenants
@@ -24,8 +23,8 @@ CREATE TABLE
slug VARCHAR(128) NOT NULL,
description VARCHAR(128),
expire_at timestamp NOT NULL,
created_at timestamp NOT NULL,
updated_at timestamp NOT NULL
created_at timestamp NOT NULL default now(),
updated_at timestamp NOT NULL default now()
);
-- table users_tenants
@@ -34,11 +33,13 @@ CREATE TABLE
id SERIAL8 PRIMARY KEY,
user_id INT8 NOT NULL,
tenant_id INT8 NOT NULL,
created_at timestamp NOT NULL
created_at timestamp NOT NULL default now()
);
CREATE INDEX idx_users_tenants_user_id ON users_tenants (user_id);
CREATE INDEX idx_users_tenants_tenant_id ON users_tenants (tenant_id);
-- uniq user_id, tenant_id
CREATE UNIQUE INDEX idx_users_tenants_user_id_tenant_id ON users_tenants (user_id, tenant_id);
CREATE TABLE tenant_user_balances (
id SERIAL8 PRIMARY KEY,
@@ -56,9 +57,9 @@ CREATE TABLE user_balance_histories (
user_id INT8 NOT NULL,
tenant_id INT8 NOT NULL,
balance INT8 NOT NULL,
target jsonb ,
target jsonb default '{}'::jsonb,
type VARCHAR(128) NOT NULL, -- charge, consume, refund
created_at timestamp NOT NULL
created_at timestamp NOT NULL default now()
);
CREATE INDEX idx_user_balance_histories_user_id ON user_balance_histories (user_id);
CREATE INDEX idx_user_balance_histories_tenant_id ON user_balance_histories (tenant_id);
@@ -73,8 +74,8 @@ CREATE TABLE
price INT8 NOT NULL,
discount INT8 NOT NULL,
publish BOOL NOT NULL,
created_at timestamp NOT NULL,
updated_at timestamp NOT NULL
created_at timestamp NOT NULL default now(),
updated_at timestamp NOT NULL default now()
);
CREATE INDEX idx_medias_tenant_id ON medias (tenant_id);
@@ -86,11 +87,11 @@ CREATE TABLE
id SERIAL8 PRIMARY KEY,
media_id INT8 NOT NULL,
type VARCHAR(128) NOT NULL,
source jsonb ,
source jsonb default '{}'::jsonb,
size INT8 NOT NULL,
publish BOOL NOT NULL,
created_at timestamp NOT NULL,
updated_at timestamp NOT NULL
created_at timestamp NOT NULL default now(),
updated_at timestamp NOT NULL default now()
);
CREATE INDEX idx_media_resources_media_id ON media_resources (media_id);
@@ -101,7 +102,7 @@ CREATE TABLE user_medias (
tenant_id INT8 NOT NULL,
media_id INT8 NOT NULL,
price INT8 NOT NULL,
created_at timestamp NOT NULL
created_at timestamp NOT NULL default now()
);
CREATE INDEX idx_user_medias_user_id ON user_medias (user_id);

26
backend/fixtures/db.go Normal file
View File

@@ -0,0 +1,26 @@
package fixtures
import (
"database/sql"
// . "github.com/go-jet/jet/v2/postgres"
_ "github.com/lib/pq"
"github.com/sirupsen/logrus"
)
func GetDB() (*sql.DB, error) {
logrus.SetLevel(logrus.DebugLevel)
dsn := "postgres://postgres:xixi0202@10.1.1.3:5432/qvyun?sslmode=disable"
db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, err
}
err = db.Ping()
if err != nil {
return nil, err
}
return db, nil
}

View File

@@ -12,6 +12,7 @@ import (
"backend/pkg/pg"
. "github.com/go-jet/jet/v2/postgres"
"github.com/go-jet/jet/v2/qrm"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)
@@ -27,6 +28,23 @@ func (svc *Service) Prepare() error {
return nil
}
// GetByID
func (svc *Service) GetByID(ctx context.Context, id int64) (*model.Users, error) {
tbl := table.Users
stmt := tbl.
SELECT(tbl.AllColumns).
WHERE(
tbl.ID.EQ(Int64(id)),
)
svc.log.WithField("method", "GetByID").Debug(stmt.DebugSql())
var item model.Users
if err := stmt.QueryContext(ctx, svc.db, &item); err != nil {
return nil, errors.Wrap(err, "failed to query user by id")
}
return &item, nil
}
// GetByOpenID
func (svc *Service) GetByOpenID(ctx context.Context, openid string) (*model.Users, error) {
tbl := table.Users
@@ -51,14 +69,28 @@ func (svc *Service) GetOrNew(ctx context.Context, tenantID int64, openid string,
user, err := svc.GetByOpenID(ctx, openid)
if err == nil {
// check: if tenant has user
hasUser, err := svc.TenantHasUser(ctx, user.ID, tenantID)
if err != nil {
return nil, errors.Wrap(err, "failed to check user-tenant relation")
}
if !hasUser {
// create user-tenant relation
if err := svc.CreateTenantUser(ctx, user.ID, tenantID); err != nil {
return nil, errors.Wrap(err, "failed to create user-tenant relation")
}
}
return user, nil
}
if errors.Is(err, sql.ErrNoRows) {
if errors.Is(err, qrm.ErrNoRows) {
user = &model.Users{
OpenID: openid,
OAuth: authInfo,
ExpireIn: time.Now().Add(time.Minute * time.Duration(authInfo.ExpiresIn)),
OpenID: openid,
OAuth: authInfo,
ExpireIn: time.Now().Add(time.Minute * time.Duration(authInfo.ExpiresIn)),
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
tx, err := svc.db.BeginTx(ctx, nil)
@@ -93,17 +125,16 @@ func (svc *Service) GetOrNew(ctx context.Context, tenantID int64, openid string,
func (svc *Service) CreateFromModel(ctx context.Context, user *model.Users) (*model.Users, error) {
log := svc.log.WithField("method", "CreateFromModel")
stmt := table.Users.INSERT().MODEL(user).RETURNING(table.Users.AllColumns)
tbl := table.Users
stmt := tbl.INSERT(tbl.AllColumns.Except(tbl.ID)).MODEL(user).RETURNING(tbl.AllColumns)
log.Debug(stmt.DebugSql())
// get tx from context
var item model.Users
if err := stmt.QueryContext(ctx, db.FromContext(ctx, svc.db), &item); err != nil {
var userModel model.Users
err := stmt.QueryContext(ctx, db.FromContext(ctx, svc.db), &userModel)
if err != nil {
return nil, errors.Wrap(err, "failed to create user")
}
return &item, nil
return &userModel, nil
}
// GetTenantByID
@@ -152,7 +183,10 @@ func (svc *Service) CreateTenantUser(ctx context.Context, userID, tenantID int64
).VALUES(
Int64(userID),
Int64(tenantID),
)
).ON_CONFLICT(
table.UsersTenants.UserID,
table.UsersTenants.TenantID,
).DO_NOTHING()
log.Debug(stmt.DebugSql())
if _, err := stmt.ExecContext(ctx, db.FromContext(ctx, svc.db)); err != nil {

View File

@@ -0,0 +1,67 @@
package users
import (
"context"
"testing"
"backend/fixtures"
dbUtil "backend/pkg/db"
"backend/pkg/pg"
. "github.com/smartystreets/goconvey/convey"
)
func TestService_GetOrNew(t *testing.T) {
FocusConvey("Test GetOrNew", t, func() {
// So(dbUtil.TruncateAllTables(context.TODO(), db, "users", "users_tenants"), ShouldBeNil)
db, err := fixtures.GetDB()
So(err, ShouldBeNil)
defer db.Close()
Convey("Test GetOrNew", func() {
svc := &Service{db: db}
So(svc.Prepare(), ShouldBeNil)
user, err := svc.GetByOpenID(context.Background(), "hello")
So(err, ShouldBeNil)
So(user, ShouldNotBeNil)
So(user.OpenID, ShouldEqual, "hello")
})
FocusConvey("Test GetOrNew", func() {
svc := &Service{db: db}
So(svc.Prepare(), ShouldBeNil)
openid := "test_openid"
authInfo := pg.UserOAuth{
AccessToken: "test_access_token",
}
user, err := svc.GetOrNew(context.Background(), 1, openid, authInfo)
So(err, ShouldBeNil)
So(user.OpenID, ShouldEqual, openid)
})
})
}
func TestService_CreateTenantUser(t *testing.T) {
FocusConvey("Test CreateTenantUser", t, func() {
db, err := fixtures.GetDB()
So(err, ShouldBeNil)
defer db.Close()
So(dbUtil.TruncateAllTables(context.TODO(), db, "users", "users_tenants"), ShouldBeNil)
FocusConvey("Test Create", func() {
svc := &Service{db: db}
So(svc.Prepare(), ShouldBeNil)
err := svc.CreateTenantUser(context.Background(), 1, 1)
So(err, ShouldBeNil)
err = svc.CreateTenantUser(context.Background(), 1, 1)
So(err, ShouldBeNil)
})
})
}

View File

@@ -3,6 +3,7 @@ package db
import (
"context"
"database/sql"
"fmt"
"backend/common/consts"
@@ -15,3 +16,13 @@ func FromContext(ctx context.Context, db *sql.DB) qrm.DB {
}
return db
}
func TruncateAllTables(ctx context.Context, db *sql.DB, tableName ...string) error {
for _, name := range tableName {
sql := fmt.Sprintf("TRUNCATE TABLE %s CASCADE", name)
if _, err := db.ExecContext(ctx, sql); err != nil {
return err
}
}
return nil
}

View File

@@ -1,11 +1,33 @@
package pg
import (
"database/sql/driver"
"encoding/json"
"errors"
)
type UserOAuth struct {
AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires_in"`
IsSnapshotuser int64 `json:"is_snapshotuser"`
Openid string `json:"openid"`
RefreshToken string `json:"refresh_token"`
Scope string `json:"scope"`
Unionid string `json:"unionid"`
AccessToken string `json:"access_token,omitempty"`
ExpiresIn int64 `json:"expires_in,omitempty"`
IsSnapshotuser int64 `json:"is_snapshotuser,omitempty"`
Openid string `json:"openid,omitempty"`
RefreshToken string `json:"refresh_token,omitempty"`
Scope string `json:"scope,omitempty"`
Unionid string `json:"unionid,omitempty"`
}
func (x UserOAuth) Scan(value interface{}) (err error) {
switch v := value.(type) {
case string:
return json.Unmarshal([]byte(v), &x)
case []byte:
return json.Unmarshal(v, &x)
case *string:
return json.Unmarshal([]byte(*v), &x)
}
return errors.New("Unknown type for ")
}
func (x UserOAuth) Value() (driver.Value, error) {
return json.Marshal(x)
}

Submodule backend/providers/app updated: 6ea394c474...9de1e7b7b0