feat: uniq tenant_users
This commit is contained in:
@@ -4,16 +4,15 @@
|
|||||||
CREATE TABLE
|
CREATE TABLE
|
||||||
users (
|
users (
|
||||||
id SERIAL8 PRIMARY KEY,
|
id SERIAL8 PRIMARY KEY,
|
||||||
open_id VARCHAR(128) NOT NULL,
|
open_id VARCHAR(128) NOT NULL UNIQUE,
|
||||||
union_id VARCHAR(128) ,
|
union_id VARCHAR(128) ,
|
||||||
oauth jsonb ,
|
oauth jsonb default '{}'::jsonb,
|
||||||
expire_in timestamp NOT NULL,
|
expire_in timestamp NOT NULL,
|
||||||
created_at timestamp NOT NULL,
|
created_at timestamp NOT NULL default now(),
|
||||||
updated_at timestamp NOT NULL
|
updated_at timestamp NOT NULL default now()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX idx_users_open_id ON users (open_id);
|
CREATE INDEX idx_users_open_id ON users (open_id);
|
||||||
|
|
||||||
CREATE INDEX idx_users_union_id ON users (union_id);
|
CREATE INDEX idx_users_union_id ON users (union_id);
|
||||||
|
|
||||||
-- table tenants
|
-- table tenants
|
||||||
@@ -24,8 +23,8 @@ CREATE TABLE
|
|||||||
slug VARCHAR(128) NOT NULL,
|
slug VARCHAR(128) NOT NULL,
|
||||||
description VARCHAR(128),
|
description VARCHAR(128),
|
||||||
expire_at timestamp NOT NULL,
|
expire_at timestamp NOT NULL,
|
||||||
created_at timestamp NOT NULL,
|
created_at timestamp NOT NULL default now(),
|
||||||
updated_at timestamp NOT NULL
|
updated_at timestamp NOT NULL default now()
|
||||||
);
|
);
|
||||||
|
|
||||||
-- table users_tenants
|
-- table users_tenants
|
||||||
@@ -34,11 +33,13 @@ CREATE TABLE
|
|||||||
id SERIAL8 PRIMARY KEY,
|
id SERIAL8 PRIMARY KEY,
|
||||||
user_id INT8 NOT NULL,
|
user_id INT8 NOT NULL,
|
||||||
tenant_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_user_id ON users_tenants (user_id);
|
||||||
CREATE INDEX idx_users_tenants_tenant_id ON users_tenants (tenant_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 (
|
CREATE TABLE tenant_user_balances (
|
||||||
id SERIAL8 PRIMARY KEY,
|
id SERIAL8 PRIMARY KEY,
|
||||||
@@ -56,9 +57,9 @@ CREATE TABLE user_balance_histories (
|
|||||||
user_id INT8 NOT NULL,
|
user_id INT8 NOT NULL,
|
||||||
tenant_id INT8 NOT NULL,
|
tenant_id INT8 NOT NULL,
|
||||||
balance INT8 NOT NULL,
|
balance INT8 NOT NULL,
|
||||||
target jsonb ,
|
target jsonb default '{}'::jsonb,
|
||||||
type VARCHAR(128) NOT NULL, -- charge, consume, refund
|
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_user_id ON user_balance_histories (user_id);
|
||||||
CREATE INDEX idx_user_balance_histories_tenant_id ON user_balance_histories (tenant_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,
|
price INT8 NOT NULL,
|
||||||
discount INT8 NOT NULL,
|
discount INT8 NOT NULL,
|
||||||
publish BOOL NOT NULL,
|
publish BOOL NOT NULL,
|
||||||
created_at timestamp NOT NULL,
|
created_at timestamp NOT NULL default now(),
|
||||||
updated_at timestamp NOT NULL
|
updated_at timestamp NOT NULL default now()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX idx_medias_tenant_id ON medias (tenant_id);
|
CREATE INDEX idx_medias_tenant_id ON medias (tenant_id);
|
||||||
@@ -86,11 +87,11 @@ CREATE TABLE
|
|||||||
id SERIAL8 PRIMARY KEY,
|
id SERIAL8 PRIMARY KEY,
|
||||||
media_id INT8 NOT NULL,
|
media_id INT8 NOT NULL,
|
||||||
type VARCHAR(128) NOT NULL,
|
type VARCHAR(128) NOT NULL,
|
||||||
source jsonb ,
|
source jsonb default '{}'::jsonb,
|
||||||
size INT8 NOT NULL,
|
size INT8 NOT NULL,
|
||||||
publish BOOL NOT NULL,
|
publish BOOL NOT NULL,
|
||||||
created_at timestamp NOT NULL,
|
created_at timestamp NOT NULL default now(),
|
||||||
updated_at timestamp NOT NULL
|
updated_at timestamp NOT NULL default now()
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX idx_media_resources_media_id ON media_resources (media_id);
|
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,
|
tenant_id INT8 NOT NULL,
|
||||||
media_id INT8 NOT NULL,
|
media_id INT8 NOT NULL,
|
||||||
price 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);
|
CREATE INDEX idx_user_medias_user_id ON user_medias (user_id);
|
||||||
|
|||||||
26
backend/fixtures/db.go
Normal file
26
backend/fixtures/db.go
Normal 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
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"backend/pkg/pg"
|
"backend/pkg/pg"
|
||||||
|
|
||||||
. "github.com/go-jet/jet/v2/postgres"
|
. "github.com/go-jet/jet/v2/postgres"
|
||||||
|
"github.com/go-jet/jet/v2/qrm"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
@@ -27,6 +28,23 @@ func (svc *Service) Prepare() error {
|
|||||||
return nil
|
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
|
// GetByOpenID
|
||||||
func (svc *Service) GetByOpenID(ctx context.Context, openid string) (*model.Users, error) {
|
func (svc *Service) GetByOpenID(ctx context.Context, openid string) (*model.Users, error) {
|
||||||
tbl := table.Users
|
tbl := table.Users
|
||||||
@@ -51,14 +69,28 @@ func (svc *Service) GetOrNew(ctx context.Context, tenantID int64, openid string,
|
|||||||
user, err := svc.GetByOpenID(ctx, openid)
|
user, err := svc.GetByOpenID(ctx, openid)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// check: if tenant has user
|
// 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
|
return user, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
if errors.Is(err, qrm.ErrNoRows) {
|
||||||
user = &model.Users{
|
user = &model.Users{
|
||||||
OpenID: openid,
|
OpenID: openid,
|
||||||
OAuth: authInfo,
|
OAuth: authInfo,
|
||||||
ExpireIn: time.Now().Add(time.Minute * time.Duration(authInfo.ExpiresIn)),
|
ExpireIn: time.Now().Add(time.Minute * time.Duration(authInfo.ExpiresIn)),
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := svc.db.BeginTx(ctx, nil)
|
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) {
|
func (svc *Service) CreateFromModel(ctx context.Context, user *model.Users) (*model.Users, error) {
|
||||||
log := svc.log.WithField("method", "CreateFromModel")
|
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())
|
log.Debug(stmt.DebugSql())
|
||||||
|
|
||||||
// get tx from context
|
var userModel model.Users
|
||||||
|
err := stmt.QueryContext(ctx, db.FromContext(ctx, svc.db), &userModel)
|
||||||
var item model.Users
|
if err != nil {
|
||||||
if err := stmt.QueryContext(ctx, db.FromContext(ctx, svc.db), &item); err != nil {
|
|
||||||
return nil, errors.Wrap(err, "failed to create user")
|
return nil, errors.Wrap(err, "failed to create user")
|
||||||
}
|
}
|
||||||
|
return &userModel, nil
|
||||||
return &item, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTenantByID
|
// GetTenantByID
|
||||||
@@ -152,7 +183,10 @@ func (svc *Service) CreateTenantUser(ctx context.Context, userID, tenantID int64
|
|||||||
).VALUES(
|
).VALUES(
|
||||||
Int64(userID),
|
Int64(userID),
|
||||||
Int64(tenantID),
|
Int64(tenantID),
|
||||||
)
|
).ON_CONFLICT(
|
||||||
|
table.UsersTenants.UserID,
|
||||||
|
table.UsersTenants.TenantID,
|
||||||
|
).DO_NOTHING()
|
||||||
log.Debug(stmt.DebugSql())
|
log.Debug(stmt.DebugSql())
|
||||||
|
|
||||||
if _, err := stmt.ExecContext(ctx, db.FromContext(ctx, svc.db)); err != nil {
|
if _, err := stmt.ExecContext(ctx, db.FromContext(ctx, svc.db)); err != nil {
|
||||||
|
|||||||
67
backend/modules/users/service_test.go
Normal file
67
backend/modules/users/service_test.go
Normal 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)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ package db
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"backend/common/consts"
|
"backend/common/consts"
|
||||||
|
|
||||||
@@ -15,3 +16,13 @@ func FromContext(ctx context.Context, db *sql.DB) qrm.DB {
|
|||||||
}
|
}
|
||||||
return 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,11 +1,33 @@
|
|||||||
package pg
|
package pg
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
type UserOAuth struct {
|
type UserOAuth struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token,omitempty"`
|
||||||
ExpiresIn int64 `json:"expires_in"`
|
ExpiresIn int64 `json:"expires_in,omitempty"`
|
||||||
IsSnapshotuser int64 `json:"is_snapshotuser"`
|
IsSnapshotuser int64 `json:"is_snapshotuser,omitempty"`
|
||||||
Openid string `json:"openid"`
|
Openid string `json:"openid,omitempty"`
|
||||||
RefreshToken string `json:"refresh_token"`
|
RefreshToken string `json:"refresh_token,omitempty"`
|
||||||
Scope string `json:"scope"`
|
Scope string `json:"scope,omitempty"`
|
||||||
Unionid string `json:"unionid"`
|
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
Reference in New Issue
Block a user