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
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"gorm.io/gorm"
|
||||
"kra/pkg/database/migration"
|
||||
)
|
||||
|
||||
func Migrations() []migration.Step {
|
||||
return []migration.Step{{ID: "202611030001_business_preference", Migrate: func(db *gorm.DB) error {
|
||||
// 旧表沿用;已迁移数据库不重建或重置用户偏好。
|
||||
if err := migration.CreateMissingTables(db, &preferencePO{}); err != nil {
|
||||
return err
|
||||
}
|
||||
if !db.Migrator().HasTable("sys_authorities") || !db.Migrator().HasTable("casbin_rule") {
|
||||
return nil
|
||||
}
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
// 两个接口仅操作当前身份,旧系统对所有已登录用户开放。
|
||||
for _, method := range []string{"GET", "PUT"} {
|
||||
if err := tx.Exec(`INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5)
|
||||
SELECT 'p', a.authority_id, ?, ?, '', '', '' FROM sys_authorities a
|
||||
WHERE a.deleted_at IS NULL AND NOT EXISTS (
|
||||
SELECT 1 FROM casbin_rule p WHERE p.ptype='p' AND p.v0=a.authority_id AND p.v1=? AND p.v2=?)`,
|
||||
"/sw/user/preference", method, "/sw/user/preference", method).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}}}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
"kra/internal/biz/business"
|
||||
)
|
||||
|
||||
// Provider 复用 Data 持有的连接,不在仓储中创建数据库客户端。
|
||||
type Provider interface{ DB() *gorm.DB }
|
||||
|
||||
type preferencePO struct {
|
||||
UserID uint `gorm:"primaryKey;autoIncrement:false"`
|
||||
IncludeSubPromote *int
|
||||
IDCardDedup *int `gorm:"column:idcard_dedup"`
|
||||
UpdateTime time.Time
|
||||
}
|
||||
|
||||
func (preferencePO) TableName() string { return "sys_user_preference" }
|
||||
|
||||
type userPreferenceRepo struct{ data Provider }
|
||||
|
||||
func NewUserPreferenceRepo(data Provider) business.UserPreferenceRepo {
|
||||
return &userPreferenceRepo{data: data}
|
||||
}
|
||||
|
||||
func (r *userPreferenceRepo) Find(ctx context.Context, id uint) (*business.UserPreference, error) {
|
||||
var po preferencePO
|
||||
err := r.data.DB().WithContext(ctx).First(&po, "user_id = ?", id).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, business.ErrStorage
|
||||
}
|
||||
return &business.UserPreference{
|
||||
IncludeSubPromote: po.IncludeSubPromote == nil || *po.IncludeSubPromote != 0,
|
||||
IDCardDedup: po.IDCardDedup == nil || *po.IDCardDedup != 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *userPreferenceRepo) Save(ctx context.Context, id uint, pref business.UserPreference) error {
|
||||
include, dedup := 0, 0
|
||||
if pref.IncludeSubPromote {
|
||||
include = 1
|
||||
}
|
||||
if pref.IDCardDedup {
|
||||
dedup = 1
|
||||
}
|
||||
po := preferencePO{UserID: id, IncludeSubPromote: &include, IDCardDedup: &dedup, UpdateTime: time.Now()}
|
||||
if err := r.data.DB().WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "user_id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"include_sub_promote", "idcard_dedup", "update_time"}),
|
||||
}).Create(&po).Error; err != nil {
|
||||
return business.ErrStorage
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/biz/business"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testData struct{ db *gorm.DB }
|
||||
|
||||
func (d testData) DB() *gorm.DB { return d.db }
|
||||
|
||||
func openTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "business.db")), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = sqlDB.Close() })
|
||||
for _, step := range Migrations() {
|
||||
if err := step.Migrate(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func TestPreferencePreservesLegacyValuesAndUpserts(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
r := NewUserPreferenceRepo(testData{db})
|
||||
ctx := context.Background()
|
||||
if pref, err := r.Find(ctx, 5); err != nil || pref != nil {
|
||||
t.Fatalf("缺省记录错误: %+v %v", pref, err)
|
||||
}
|
||||
if err := db.Exec("INSERT INTO sys_user_preference(user_id,include_sub_promote,idcard_dedup) VALUES (5,NULL,2),(6,1,1)").Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pref, err := r.Find(ctx, 5)
|
||||
if err != nil || !pref.IncludeSubPromote || !pref.IDCardDedup {
|
||||
t.Fatalf("旧 NULL 和非零值应为开启: %+v %v", pref, err)
|
||||
}
|
||||
for i := 0; i < 2; i++ {
|
||||
if err := r.Save(ctx, 5, business.UserPreference{}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
for _, step := range Migrations() {
|
||||
if err := step.Migrate(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
pref, err = r.Find(ctx, 5)
|
||||
if err != nil || pref.IncludeSubPromote || pref.IDCardDedup {
|
||||
t.Fatalf("重复迁移不能重置偏好: %+v %v", pref, err)
|
||||
}
|
||||
other, err := r.Find(ctx, 6)
|
||||
if err != nil || !other.IncludeSubPromote || !other.IDCardDedup {
|
||||
t.Fatalf("影响其他用户: %+v %v", other, err)
|
||||
}
|
||||
var count int64
|
||||
db.Model(&preferencePO{}).Where("user_id = ?", 5).Count(&count)
|
||||
if count != 1 {
|
||||
t.Fatalf("重复写入产生多条记录: %d", count)
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"gorm.io/gorm"
|
||||
"kra/internal/config"
|
||||
databusiness "kra/internal/data/business"
|
||||
dataintegration "kra/internal/data/integration"
|
||||
datapayment "kra/internal/data/payment"
|
||||
datasystem "kra/internal/data/system"
|
||||
@@ -25,6 +26,8 @@ import (
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
NewData,
|
||||
wire.Bind(new(databusiness.Provider), new(*Data)),
|
||||
databusiness.NewUserPreferenceRepo,
|
||||
NewIntegrationRuntime,
|
||||
wire.Bind(new(datasystem.Provider), new(*Data)),
|
||||
wire.Bind(new(datasystem.DatabaseProvider), new(*Data)),
|
||||
|
||||
@@ -34,8 +34,8 @@ func TestMigrateAllRunsModuleSchemasWithoutBootstrapSeed(t *testing.T) {
|
||||
if err = db.Table(migration.TableName).Count(&versions).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if versions != 14 {
|
||||
t.Fatalf("migration versions = %d, want 14", versions)
|
||||
if versions != 16 {
|
||||
t.Fatalf("migration versions = %d, want 16", versions)
|
||||
}
|
||||
var communicationRows []dataintegration.ConfigPO
|
||||
if err = db.Where("kind IN ?", []string{"mq", "websocket"}).Order("kind, provider").Find(&communicationRows).Error; err != nil {
|
||||
|
||||
@@ -178,7 +178,7 @@ func (r *authorityAccessRepo) CreateAuthority(ctx context.Context, value *system
|
||||
value.Menus = nil
|
||||
}
|
||||
}
|
||||
defaults := []struct{ path, method string }{{"/menu/getMenu", "POST"}, {"/jwt/jsonInBlacklist", "POST"}, {"/base/login", "POST"}, {"/user/changePassword", "POST"}, {"/user/setUserAuthority", "POST"}, {"/user/getUserInfo", "GET"}, {"/user/setSelfInfo", "PUT"}, {"/fileUploadAndDownload/upload", "POST"}, {"/sysDictionary/findSysDictionary", "GET"}}
|
||||
defaults := []struct{ path, method string }{{"/sw/user/preference", "GET"}, {"/sw/user/preference", "PUT"}, {"/menu/getMenu", "POST"}, {"/jwt/jsonInBlacklist", "POST"}, {"/base/login", "POST"}, {"/user/changePassword", "POST"}, {"/user/setUserAuthority", "POST"}, {"/user/getUserInfo", "GET"}, {"/user/setSelfInfo", "PUT"}, {"/fileUploadAndDownload/upload", "POST"}, {"/sysDictionary/findSysDictionary", "GET"}}
|
||||
rules := make([]casbinRulePO, 0, len(defaults))
|
||||
for _, item := range defaults {
|
||||
if strict {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package system
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBusinessFieldsSurviveMigrationAndIdentityLoad(t *testing.T) {
|
||||
db, err := openWithDriver("sqlite", "file:"+t.Name()+"?mode=memory&cache=shared")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = migrateAll(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
po := userPO{Username: "business", UUID: "business-uuid", Password: "original-hash", Enable: 1, Level: 2, ParentID: 11, IDPath: "0,11", RealName: "商务姓名", QQNumber: "12345", BusinessStatus: 3, RejectReason: "审核资料"}
|
||||
if err = db.Create(&po).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = ensureBusinessUserFields(db); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := NewUserRepo(&Data{gormDB: newReloadableDB(db, nil)})
|
||||
user, err := r.FindUserByUUID(context.Background(), "business-uuid")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if user.Level != 2 || user.ParentID != 11 || user.IDPath != "0,11" || user.RealName != "商务姓名" || user.QQNumber != "12345" || user.BusinessStatus != 3 || user.RejectReason != "审核资料" || user.Password != "original-hash" {
|
||||
t.Fatalf("迁移或身份加载丢失商务字段: %+v", user)
|
||||
}
|
||||
}
|
||||
@@ -30,9 +30,25 @@ func Migrations() []migration.Step {
|
||||
},
|
||||
{ID: "202608210002_communication_surface", Migrate: ensureCommunicationSurface},
|
||||
{ID: "202608210003_communication_test_surface", Migrate: ensureCommunicationTestSurface},
|
||||
{ID: "202611030002_business_user_fields", Migrate: ensureBusinessUserFields},
|
||||
}
|
||||
}
|
||||
|
||||
func ensureBusinessUserFields(db *gorm.DB) error {
|
||||
// 与已交付迁移 SQL 的字段保持一致,仅补缺列,保留导入值和密码。
|
||||
for _, field := range []string{"Level", "ParentID", "IDPath", "RealName", "QQNumber", "BusinessStatus", "RejectReason"} {
|
||||
if !db.Migrator().HasColumn(&userPO{}, field) {
|
||||
if err := db.Migrator().AddColumn(&userPO{}, field); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if !db.Migrator().HasIndex(&userPO{}, "idx_sys_users_business_parent") {
|
||||
return db.Migrator().CreateIndex(&userPO{}, "idx_sys_users_business_parent")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Kept as a migration version for existing installations; the canonical
|
||||
// communication surface seeder is idempotent and now owns the test endpoint.
|
||||
func ensureCommunicationTestSurface(db *gorm.DB) error { return ensureCommunicationSurface(db) }
|
||||
|
||||
@@ -25,6 +25,13 @@ type userPO struct {
|
||||
OriginSetting string `gorm:"type:text"`
|
||||
PasswordUpdatedAt *time.Time
|
||||
MustChangePassword bool
|
||||
Level int `gorm:"type:tinyint unsigned;not null;default:0"`
|
||||
ParentID uint `gorm:"not null;default:0;index:idx_sys_users_business_parent"`
|
||||
IDPath string `gorm:"column:id_path;size:255;not null;default:0"`
|
||||
RealName string `gorm:"size:30;not null;default:''"`
|
||||
QQNumber string `gorm:"column:qq_number;size:20;not null;default:''"`
|
||||
BusinessStatus int `gorm:"type:tinyint unsigned;not null;default:0"`
|
||||
RejectReason string `gorm:"size:200;not null;default:''"`
|
||||
}
|
||||
|
||||
func (userPO) TableName() string { return "sys_users" }
|
||||
|
||||
@@ -93,7 +93,7 @@ func (r *userRepo) loadUser(ctx context.Context, po *userPO) (*system.User, erro
|
||||
for _, item := range positionPOs {
|
||||
positions = append(positions, *posFromPO(item))
|
||||
}
|
||||
return &system.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, Authority: toBizAuthority(authority), Authorities: authorities, DeptID: po.DeptID, Department: primary, Departments: departments, Positions: positions, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt}, nil
|
||||
return &system.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, Authority: toBizAuthority(authority), Authorities: authorities, DeptID: po.DeptID, Department: primary, Departments: departments, Positions: positions, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt, Level: po.Level, ParentID: po.ParentID, IDPath: po.IDPath, RealName: po.RealName, QQNumber: po.QQNumber, BusinessStatus: po.BusinessStatus, RejectReason: po.RejectReason}, nil
|
||||
}
|
||||
|
||||
func toBizAuthority(po authorityPO) system.Authority {
|
||||
@@ -116,7 +116,7 @@ func baseBizUser(po *userPO) (*system.User, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &system.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, DeptID: po.DeptID, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt}, nil
|
||||
return &system.User{ID: po.ID, CreatedAt: po.CreatedAt, UpdatedAt: po.UpdatedAt, UUID: po.UUID, Username: po.Username, Password: po.Password, NickName: po.NickName, HeaderImg: po.HeaderImg, AuthorityID: po.AuthorityID, DeptID: po.DeptID, Phone: po.Phone, Email: po.Email, Enable: po.Enable, OriginSetting: setting, MustChangePassword: po.MustChangePassword, PasswordUpdatedAt: po.PasswordUpdatedAt, Level: po.Level, ParentID: po.ParentID, IDPath: po.IDPath, RealName: po.RealName, QQNumber: po.QQNumber, BusinessStatus: po.BusinessStatus, RejectReason: po.RejectReason}, nil
|
||||
}
|
||||
|
||||
func (r *userRepo) loadUsers(ctx context.Context, pos []userPO) ([]*system.User, error) {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
databusiness "kra/internal/data/business"
|
||||
"kra/internal/modules/surface"
|
||||
"kra/pkg/module"
|
||||
)
|
||||
|
||||
func Definition() module.Definition {
|
||||
return module.Definition{Name: "business", Migrations: databusiness.Migrations(), Surface: module.Surface{
|
||||
APIs: surface.APIsForPrefix("/sw/", 0),
|
||||
}}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
package modules
|
||||
|
||||
import (
|
||||
businessmodule "kra/internal/modules/business"
|
||||
integrationmodule "kra/internal/modules/integration"
|
||||
paymentmodule "kra/internal/modules/payment"
|
||||
systemmodule "kra/internal/modules/system"
|
||||
@@ -20,5 +21,6 @@ func Catalog() module.Catalog {
|
||||
integrationmodule.Definition(),
|
||||
taskmodule.Definition(),
|
||||
paymentmodule.Definition(),
|
||||
businessmodule.Definition(),
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -16,25 +16,25 @@ func TestCatalogMigrationIDsAreUnique(t *testing.T) {
|
||||
|
||||
func TestCatalogContainsBuiltInModulesInDependencyOrder(t *testing.T) {
|
||||
catalog := Catalog()
|
||||
if len(catalog.Definitions) != 4 {
|
||||
t.Fatalf("definitions = %d, want 4", len(catalog.Definitions))
|
||||
if len(catalog.Definitions) != 5 {
|
||||
t.Fatalf("definitions = %d, want 5", len(catalog.Definitions))
|
||||
}
|
||||
want := []string{"system", "integration", "task", "payment"}
|
||||
want := []string{"system", "integration", "task", "payment", "business"}
|
||||
for index, name := range want {
|
||||
if got := catalog.Definitions[index].Name; got != name {
|
||||
t.Fatalf("definition[%d] = %q, want %q", index, got, name)
|
||||
}
|
||||
}
|
||||
if got := catalog.MigrationSteps(); len(got) != 14 {
|
||||
t.Fatalf("module migrations = %d, want 14", len(got))
|
||||
if got := catalog.MigrationSteps(); len(got) != 16 {
|
||||
t.Fatalf("module migrations = %d, want 16", len(got))
|
||||
}
|
||||
var menus, apis int
|
||||
for _, definition := range catalog.Definitions {
|
||||
menus += len(definition.Surface.Menus)
|
||||
apis += len(definition.Surface.APIs)
|
||||
}
|
||||
if menus != 4 || apis != 18 {
|
||||
t.Fatalf("admin surface = %d menus/%d APIs, want 4/18", menus, apis)
|
||||
if menus != 4 || apis != 20 {
|
||||
t.Fatalf("admin surface = %d menus/%d APIs, want 4/20", menus, apis)
|
||||
}
|
||||
if got := catalog.DefaultTimedTasks(); len(got) != 2 {
|
||||
t.Fatalf("default timed tasks = %d, want 2", len(got))
|
||||
|
||||
@@ -41,6 +41,8 @@ type routeValue struct {
|
||||
// Gin router files still bind handlers; tests keep those registrations aligned
|
||||
// with this catalog while public, audit and API metadata live only here.
|
||||
var routes = map[string]routeValue{
|
||||
"GET /sw/user/preference": {group: "商务偏好", description: "获取当前用户商务偏好"},
|
||||
"PUT /sw/user/preference": {group: "商务偏好", description: "更新当前用户商务偏好", audit: true},
|
||||
"DELETE /api/deleteApisByIds": {group: "api", description: "批量删除api", audit: true},
|
||||
"DELETE /dataAccessLog/deleteDataAccessLogByIds": {group: "数据权限审计", description: "批量删除数据权限审计日志", audit: true},
|
||||
"DELETE /department/deleteDepartment": {group: "部门", description: "删除部门", audit: true},
|
||||
|
||||
@@ -26,6 +26,7 @@ func NewGinEngine(runtime *config.Store, access *systemservice.AccessControlServ
|
||||
|
||||
func emptyHandlers() *handler.Set {
|
||||
return &handler.Set{
|
||||
Business: &handler.Business{},
|
||||
Authority: &handler.Authority{}, Menu: &handler.Menu{}, API: &handler.API{},
|
||||
Permission: &handler.Permission{}, Organization: &handler.Organization{},
|
||||
Announcement: &handler.Announcement{}, Email: &handler.Email{}, Payment: &handler.Payment{}, Task: &handler.Task{},
|
||||
@@ -97,7 +98,7 @@ func TestGinStartupLogsEveryRegisteredRoute(t *testing.T) {
|
||||
if got, want := strings.Count(text, `"msg":"router registered"`), len(engine.Routes()); got != want {
|
||||
t.Fatalf("registered route log count = %d, want %d", got, want)
|
||||
}
|
||||
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":197`) {
|
||||
if !strings.Contains(text, `"msg":"router register success"`) || !strings.Contains(text, `"route_count":199`) {
|
||||
t.Fatalf("startup route summary is missing: %s", text)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/go-kratos/kratos/v3/errors"
|
||||
"kra/internal/service/business"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type Business struct {
|
||||
preference *business.UserPreferenceService
|
||||
}
|
||||
|
||||
func NewBusiness(preference *business.UserPreferenceService) *Business {
|
||||
return &Business{preference: preference}
|
||||
}
|
||||
|
||||
func (h *Business) UserPreference(c *gin.Context) {
|
||||
value, err := h.preference.Current(c.Request.Context())
|
||||
if err != nil {
|
||||
Fail(c, errors.FromError(err).Message)
|
||||
return
|
||||
}
|
||||
OKWithData(c, value)
|
||||
}
|
||||
|
||||
func (h *Business) UpdateUserPreference(c *gin.Context) {
|
||||
var req dto.UserPreferenceRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
Fail(c, "偏好参数格式错误")
|
||||
return
|
||||
}
|
||||
if err := h.preference.UpdateCurrent(c.Request.Context(), &req); err != nil {
|
||||
Fail(c, errors.FromError(err).Message)
|
||||
return
|
||||
}
|
||||
OK(c)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import "github.com/google/wire"
|
||||
// ProviderSet wires the system HTTP handlers. Set itself is filled field by
|
||||
// field so adding a handler only means adding its constructor here.
|
||||
var ProviderSet = wire.NewSet(
|
||||
NewBusiness,
|
||||
NewAuthority, NewMenu, NewAPI, NewPermission, NewOrganization,
|
||||
NewAnnouncement, NewEmail, NewPayment, NewTask, NewMedia, NewAudit,
|
||||
NewExport, NewVersion, NewDictionary, NewParameter, NewAPIToken,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
type Set struct {
|
||||
Business *Business
|
||||
SMS *SMS
|
||||
Authority *Authority
|
||||
Menu *Menu
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"kra/internal/server/handler"
|
||||
)
|
||||
|
||||
func RegisterBusiness(private *gin.RouterGroup, h *handler.Business) {
|
||||
private.GET("/sw/user/preference", h.UserPreference)
|
||||
private.PUT("/sw/user/preference", h.UpdateUserPreference)
|
||||
}
|
||||
@@ -20,6 +20,7 @@ func (r *Routes) RegisterRoutes(public, private *gin.RouterGroup, engine *gin.En
|
||||
return
|
||||
}
|
||||
registrations := []func(){
|
||||
func() { RegisterBusiness(private, r.handlers.Business) },
|
||||
func() { RegisterPublic(public, engine, r.handlers.Public) },
|
||||
func() { RegisterUser(private, r.handlers.User) },
|
||||
func() { RegisterNavigation(private, r.handlers.Navigation) },
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/service/dto"
|
||||
)
|
||||
|
||||
type PreferenceUsecase interface {
|
||||
Current(context.Context) (*business.UserPreference, error)
|
||||
UpdateCurrent(context.Context, business.UserPreference) error
|
||||
}
|
||||
|
||||
type UserPreferenceService struct{ uc PreferenceUsecase }
|
||||
|
||||
func NewUserPreferenceService(uc *business.UserPreferenceUsecase) *UserPreferenceService {
|
||||
return &UserPreferenceService{uc: uc}
|
||||
}
|
||||
|
||||
func (s *UserPreferenceService) Current(ctx context.Context) (*dto.UserPreferenceResponse, error) {
|
||||
pref, err := s.uc.Current(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.UserPreferenceResponse{IncludeSubPromote: pref.IncludeSubPromote, IDCardDedup: pref.IDCardDedup}, nil
|
||||
}
|
||||
|
||||
func (s *UserPreferenceService) UpdateCurrent(ctx context.Context, req *dto.UserPreferenceRequest) error {
|
||||
if req == nil || req.IncludeSubPromote == nil || req.IDCardDedup == nil {
|
||||
return business.ErrPreferenceRequired
|
||||
}
|
||||
return s.uc.UpdateCurrent(ctx, business.UserPreference{IncludeSubPromote: *req.IncludeSubPromote, IDCardDedup: *req.IDCardDedup})
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package business
|
||||
|
||||
import (
|
||||
"context"
|
||||
"kra/internal/biz/business"
|
||||
"kra/internal/service/dto"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type preferenceFake struct {
|
||||
calls int
|
||||
value business.UserPreference
|
||||
}
|
||||
|
||||
func (f *preferenceFake) Current(context.Context) (*business.UserPreference, error) {
|
||||
return &f.value, nil
|
||||
}
|
||||
func (f *preferenceFake) UpdateCurrent(_ context.Context, p business.UserPreference) error {
|
||||
f.calls++
|
||||
f.value = p
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestPreferenceRequiresBothFieldsButAcceptsFalse(t *testing.T) {
|
||||
f := &preferenceFake{}
|
||||
s := &UserPreferenceService{uc: f}
|
||||
off := false
|
||||
for _, req := range []*dto.UserPreferenceRequest{nil, {}, {IncludeSubPromote: &off}, {IDCardDedup: &off}} {
|
||||
if err := s.UpdateCurrent(context.Background(), req); err != business.ErrPreferenceRequired {
|
||||
t.Fatalf("缺字段应被拒绝: %v", err)
|
||||
}
|
||||
}
|
||||
if f.calls != 0 {
|
||||
t.Fatal("校验失败后不应调用用例")
|
||||
}
|
||||
if err := s.UpdateCurrent(context.Background(), &dto.UserPreferenceRequest{IncludeSubPromote: &off, IDCardDedup: &off}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if f.calls != 1 || f.value.IncludeSubPromote || f.value.IDCardDedup {
|
||||
t.Fatalf("false 值未完整传递: %+v", f)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package dto
|
||||
|
||||
type UserPreferenceRequest struct {
|
||||
IncludeSubPromote *bool `json:"includeSubPromote"`
|
||||
IDCardDedup *bool `json:"idcardDedup"`
|
||||
}
|
||||
|
||||
type UserPreferenceResponse struct {
|
||||
IncludeSubPromote bool `json:"includeSubPromote"`
|
||||
IDCardDedup bool `json:"idcardDedup"`
|
||||
}
|
||||
@@ -73,6 +73,13 @@ type SwitchAuthorityRequest struct {
|
||||
}
|
||||
|
||||
type UserResponse struct {
|
||||
Level int `json:"level"`
|
||||
ParentID uint `json:"parentId"`
|
||||
IDPath string `json:"idPath"`
|
||||
RealName string `json:"realName"`
|
||||
QQNumber string `json:"qqNumber"`
|
||||
BusinessStatus int `json:"businessStatus"`
|
||||
RejectReason string `json:"rejectReason"`
|
||||
ID uint `json:"ID"`
|
||||
CreatedAt time.Time `json:"CreatedAt"`
|
||||
UpdatedAt time.Time `json:"UpdatedAt"`
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
businessservice "kra/internal/service/business"
|
||||
integrationservice "kra/internal/service/integration"
|
||||
paymentservice "kra/internal/service/payment"
|
||||
systemservice "kra/internal/service/system"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
|
||||
// ProviderSet aggregates all service modules for Wire.
|
||||
var ProviderSet = wire.NewSet(
|
||||
businessservice.NewUserPreferenceService,
|
||||
systemservice.ProviderSet,
|
||||
paymentservice.ProviderSet,
|
||||
integrationservice.ProviderSet,
|
||||
|
||||
@@ -40,5 +40,5 @@ func convertUser(user *system.User) *dto.UserResponse {
|
||||
if user.Department != nil {
|
||||
department = departmentResponse(user.Department)
|
||||
}
|
||||
return &dto.UserResponse{ID: user.ID, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, UUID: user.UUID, Username: user.Username, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Authority: convertAuthority(user.Authority), Authorities: authorities, DeptID: user.DeptID, Department: department, Departments: departments, Positions: positions, Phone: user.Phone, Email: user.Email, Enable: user.Enable, OriginSetting: user.OriginSetting, PasswordUpdatedAt: user.PasswordUpdatedAt}
|
||||
return &dto.UserResponse{ID: user.ID, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, UUID: user.UUID, Username: user.Username, NickName: user.NickName, HeaderImg: user.HeaderImg, AuthorityID: user.AuthorityID, Authority: convertAuthority(user.Authority), Authorities: authorities, DeptID: user.DeptID, Department: department, Departments: departments, Positions: positions, Phone: user.Phone, Email: user.Email, Enable: user.Enable, OriginSetting: user.OriginSetting, PasswordUpdatedAt: user.PasswordUpdatedAt, Level: user.Level, ParentID: user.ParentID, IDPath: user.IDPath, RealName: user.RealName, QQNumber: user.QQNumber, BusinessStatus: user.BusinessStatus, RejectReason: user.RejectReason}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user