feat: 接入商务用户资料和全局统计偏好
This commit is contained in:
+2
-1
@@ -1,6 +1,7 @@
|
||||
package biz
|
||||
|
||||
import (
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/biz/integration"
|
||||
"kra/internal/biz/payment"
|
||||
"kra/internal/biz/system"
|
||||
@@ -10,4 +11,4 @@ import (
|
||||
)
|
||||
|
||||
// ProviderSet is biz providers.
|
||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet)
|
||||
var ProviderSet = wire.NewSet(system.ProviderSet, payment.ProviderSet, integration.ProviderSet, task.ProviderSet, business.NewUserPreferenceUsecase)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/go-kratos/kratos/v3/errors"
|
||||
"kra/internal/biz/system"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUnauthenticated = errors.Unauthorized("SW_UNAUTHENTICATED", "请先登录")
|
||||
ErrPreferenceRequired = errors.BadRequest("SW_PREFERENCE_REQUIRED", "偏好参数不能为空")
|
||||
ErrStorage = errors.InternalServer("SW_STORAGE_ERROR", "商务数据访问失败")
|
||||
)
|
||||
|
||||
// UserPreference 使用新用户 ID,保留旧商务两个全局开关的含义。
|
||||
type UserPreference struct {
|
||||
IncludeSubPromote bool
|
||||
IDCardDedup bool
|
||||
}
|
||||
|
||||
type UserPreferenceRepo interface {
|
||||
Find(context.Context, uint) (*UserPreference, error)
|
||||
Save(context.Context, uint, UserPreference) error
|
||||
}
|
||||
|
||||
type UserPreferenceUsecase struct{ repo UserPreferenceRepo }
|
||||
|
||||
func NewUserPreferenceUsecase(repo UserPreferenceRepo) *UserPreferenceUsecase {
|
||||
return &UserPreferenceUsecase{repo: repo}
|
||||
}
|
||||
|
||||
func (uc *UserPreferenceUsecase) Current(ctx context.Context) (*UserPreference, error) {
|
||||
actor, ok := system.ActorFromContext(ctx)
|
||||
if !ok || actor.UserID == 0 {
|
||||
return nil, ErrUnauthenticated
|
||||
}
|
||||
return uc.ForUser(ctx, actor.UserID)
|
||||
}
|
||||
|
||||
func (uc *UserPreferenceUsecase) ForUser(ctx context.Context, id uint) (*UserPreference, error) {
|
||||
if id == 0 {
|
||||
return nil, ErrUnauthenticated
|
||||
}
|
||||
pref, err := uc.repo.Find(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if pref == nil {
|
||||
return &UserPreference{IncludeSubPromote: true, IDCardDedup: true}, nil
|
||||
}
|
||||
return pref, nil
|
||||
}
|
||||
|
||||
func (uc *UserPreferenceUsecase) UpdateCurrent(ctx context.Context, pref UserPreference) error {
|
||||
actor, ok := system.ActorFromContext(ctx)
|
||||
if !ok || actor.UserID == 0 {
|
||||
return ErrUnauthenticated
|
||||
}
|
||||
return uc.repo.Save(ctx, actor.UserID, pref)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"kra/internal/biz/system"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type preferenceRepoFake struct {
|
||||
value *UserPreference
|
||||
userID uint
|
||||
calls int
|
||||
}
|
||||
|
||||
func (r *preferenceRepoFake) Find(_ context.Context, id uint) (*UserPreference, error) {
|
||||
r.calls++
|
||||
r.userID = id
|
||||
return r.value, nil
|
||||
}
|
||||
func (r *preferenceRepoFake) Save(_ context.Context, id uint, pref UserPreference) error {
|
||||
r.calls++
|
||||
r.userID, r.value = id, &pref
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPreferenceDefaultsAndCurrentIdentity(t *testing.T) {
|
||||
r := &preferenceRepoFake{}
|
||||
uc := NewUserPreferenceUsecase(r)
|
||||
if _, err := uc.Current(context.Background()); err != ErrUnauthenticated || r.calls != 0 {
|
||||
t.Fatalf("匿名请求访问了偏好: %v", err)
|
||||
}
|
||||
ctx := system.NewActorContext(context.Background(), system.Actor{UserID: 7})
|
||||
pref, err := uc.Current(ctx)
|
||||
if err != nil || !pref.IncludeSubPromote || !pref.IDCardDedup {
|
||||
t.Fatalf("缺省开关应均为开启: %+v %v", pref, err)
|
||||
}
|
||||
if err = uc.UpdateCurrent(ctx, UserPreference{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if r.userID != 7 || r.value.IncludeSubPromote || r.value.IDCardDedup {
|
||||
t.Fatalf("未保存当前用户的关闭值: %+v", r)
|
||||
}
|
||||
}
|
||||
@@ -122,7 +122,7 @@ func (uc *AuthenticationUsecase) Login(ctx context.Context, attempt *LoginAttemp
|
||||
uc.recordLogin(ctx, attempt, false, "用户名不存在或者密码错误", 0)
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
if user.Enable != 1 {
|
||||
if user.Enable != 1 || user.BusinessStatus != 0 {
|
||||
_, _ = uc.security.IncrementLoginIP(ctx, attempt.IP, ipTTL)
|
||||
uc.recordLogin(ctx, attempt, false, "用户被禁止登录", user.ID)
|
||||
return nil, &UserDisabledError{UserID: user.ID}
|
||||
@@ -183,7 +183,7 @@ func (uc *AuthenticationUsecase) currentTokenUser(ctx context.Context, claims *A
|
||||
if err != nil || user == nil || user.ID != claims.ID || user.Username != claims.Username {
|
||||
return nil, ErrTokenDisabled
|
||||
}
|
||||
if user.Enable != 1 {
|
||||
if user.Enable != 1 || user.BusinessStatus != 0 {
|
||||
return nil, &UserDisabledError{UserID: user.ID}
|
||||
}
|
||||
if !userHasAuthority(user, claims.AuthorityID) {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestBusinessReviewStateBlocksPasswordAndExistingSession(t *testing.T) {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte("secret"), bcrypt.MinCost)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, status := range []int{1, 2, 3} {
|
||||
user := &User{ID: 7, Username: "business", Password: string(hash), Enable: 1, BusinessStatus: status}
|
||||
security := NewSecurityUsecase(&authenticationSecurityRepo{}, &authenticationCache{}, &authenticationSettings{}, nil)
|
||||
uc := NewAuthenticationUsecase(NewUserUsecase(&authenticationUserRepo{user: user}), security, &authenticationIssuer{}, nil)
|
||||
if _, err := uc.Login(context.Background(), &LoginAttempt{Username: "business", Password: "secret", IP: "127.0.0.1"}); !errors.Is(err, ErrUserDisabled) {
|
||||
t.Fatalf("审核状态 %d 的登录应被拒绝: %v", status, err)
|
||||
}
|
||||
if _, err := uc.currentTokenUser(context.Background(), &AuthClaims{ID: 7, Username: "business", UUID: "uuid", IssuedAt: time.Now()}); !errors.Is(err, ErrUserDisabled) {
|
||||
t.Fatalf("审核状态 %d 的现有会话应被拒绝: %v", status, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,14 @@ type User struct {
|
||||
OriginSetting map[string]any
|
||||
MustChangePassword bool
|
||||
PasswordUpdatedAt *time.Time
|
||||
// 商务资料与系统身份共用主键;平台商务 ID 另由绑定关系持有。
|
||||
Level int
|
||||
ParentID uint
|
||||
IDPath string
|
||||
RealName string
|
||||
QQNumber string
|
||||
BusinessStatus int
|
||||
RejectReason string
|
||||
}
|
||||
type UserListFilter struct {
|
||||
Username, NickName, Phone, Email, OrderKey string
|
||||
|
||||
Reference in New Issue
Block a user