feat: 迁移商务登录品牌和三步找回密码

This commit is contained in:
Yvan
2026-11-03 19:07:30 +08:00
parent 317c253ba9
commit 8048dfb514
39 changed files with 2190 additions and 283 deletions
+3 -1
View File
@@ -6,7 +6,9 @@ import (
)
func Migrations() []migration.Step {
return []migration.Step{{ID: "202611030004_business_binding_locks", Migrate: func(db *gorm.DB) error {
return []migration.Step{{ID: "202611030005_business_password_reset", Migrate: func(db *gorm.DB) error {
return migration.CreateMissingTables(db, &resetCodePO{}, &resetTokenPO{})
}}, {ID: "202611030004_business_binding_locks", Migrate: func(db *gorm.DB) error {
return migration.CreateMissingTables(db, &bindingLockPO{})
}}, {ID: "202611030003_business_bindings", Migrate: func(db *gorm.DB) error {
return migration.CreateMissingTables(db, &bindingPO{})
+128
View File
@@ -0,0 +1,128 @@
package business
import (
"context"
"errors"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"kra/internal/biz/business"
"time"
)
type resetCodePO struct {
UserID uint `gorm:"primaryKey;autoIncrement:false"`
CodeHash string `gorm:"size:64"`
ExpiresAt, SendAfter *time.Time
UpdatedAt time.Time
}
func (resetCodePO) TableName() string { return "sys_business_password_codes" }
type resetTokenPO struct {
Hash string `gorm:"column:token_hash;primaryKey;size:64"`
UserID uint `gorm:"index"`
ExpiresAt time.Time `gorm:"index"`
CreatedAt time.Time
Failures int
}
func (resetTokenPO) TableName() string { return "sys_business_password_resets" }
type passwordResetRepo struct{ data Provider }
func NewPasswordResetRepo(data Provider) business.PasswordResetRepo {
return &passwordResetRepo{data: data}
}
type resetTxKey struct{}
func (r *passwordResetRepo) db(ctx context.Context) *gorm.DB {
if tx, ok := ctx.Value(resetTxKey{}).(*gorm.DB); ok {
return tx.WithContext(ctx)
}
return r.data.DB().WithContext(ctx)
}
func resetUser(po accountPO) *business.ResetUser {
return &business.ResetUser{ID: po.ID, Username: po.Username, Phone: po.Phone, Enabled: po.Enable == 1, Status: po.BusinessStatus}
}
func (r *passwordResetRepo) UserByName(ctx context.Context, name string) (*business.ResetUser, error) {
var po accountPO
err := r.db(ctx).Select("id,username,phone,enable,business_status").Where("username = ?", name).First(&po).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, business.ErrStorage
}
return resetUser(po), nil
}
func (r *passwordResetRepo) UserByID(ctx context.Context, id uint) (*business.ResetUser, error) {
var po accountPO
err := r.db(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).Select("id,username,phone,enable,business_status").First(&po, id).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, business.ErrStorage
}
return resetUser(po), nil
}
func (r *passwordResetRepo) WithCodeLock(ctx context.Context, id uint, fn func(context.Context) error) error {
err := r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
po := resetCodePO{UserID: id}
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&po).Error; err != nil {
return err
}
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&po, id).Error; err != nil {
return err
}
return fn(context.WithValue(ctx, resetTxKey{}, tx))
})
return storageError(err)
}
func (r *passwordResetRepo) Code(ctx context.Context, id uint) (*business.ResetCode, error) {
var po resetCodePO
if err := r.db(ctx).First(&po, id).Error; err != nil {
return nil, storageError(err)
}
return &business.ResetCode{UserID: po.UserID, CodeHash: po.CodeHash, ExpiresAt: po.ExpiresAt, SendAfter: po.SendAfter}, nil
}
func (r *passwordResetRepo) SaveCode(ctx context.Context, c *business.ResetCode) error {
return storageError(r.db(ctx).Model(&resetCodePO{}).Where("user_id = ?", c.UserID).Updates(map[string]any{"code_hash": c.CodeHash, "expires_at": c.ExpiresAt, "send_after": c.SendAfter}).Error)
}
func (r *passwordResetRepo) CreateToken(ctx context.Context, t *business.ResetToken) error {
if err := r.db(ctx).Where("expires_at < ?", t.CreatedAt).Delete(&resetTokenPO{}).Error; err != nil {
return storageError(err)
}
return storageError(r.db(ctx).Create(&resetTokenPO{Hash: t.Hash, UserID: t.UserID, ExpiresAt: t.ExpiresAt, CreatedAt: t.CreatedAt, Failures: t.Failures}).Error)
}
func (r *passwordResetRepo) WithTokenLock(ctx context.Context, key string, fn func(context.Context) error) error {
return storageError(r.data.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { return fn(context.WithValue(ctx, resetTxKey{}, tx)) }))
}
func (r *passwordResetRepo) Token(ctx context.Context, key string) (*business.ResetToken, error) {
var po resetTokenPO
err := r.db(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).First(&po, "token_hash = ?", key).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
if err != nil {
return nil, storageError(err)
}
return &business.ResetToken{Hash: po.Hash, UserID: po.UserID, ExpiresAt: po.ExpiresAt, CreatedAt: po.CreatedAt, Failures: po.Failures}, nil
}
func (r *passwordResetRepo) SaveToken(ctx context.Context, t *business.ResetToken) error {
return storageError(r.db(ctx).Model(&resetTokenPO{}).Where("token_hash = ?", t.Hash).Update("failures", t.Failures).Error)
}
func (r *passwordResetRepo) DeleteToken(ctx context.Context, key string) error {
return storageError(r.db(ctx).Where("token_hash = ?", key).Delete(&resetTokenPO{}).Error)
}
func (r *passwordResetRepo) UpdatePassword(ctx context.Context, id uint, hash string) error {
result := r.db(ctx).Model(&accountPO{}).Where("id = ?", id).Updates(map[string]any{"password": hash, "password_updated_at": time.Now(), "must_change_password": false})
if result.Error != nil {
return storageError(result.Error)
}
if result.RowsAffected == 0 {
return business.ErrResetToken
}
return nil
}
+1
View File
@@ -32,6 +32,7 @@ var ProviderSet = wire.NewSet(
databusiness.NewAccountRepo,
databusiness.NewBindingRepo,
databusiness.NewPlatformAccountRepo,
databusiness.NewPasswordResetRepo,
NewIntegrationRuntime,
wire.Bind(new(datasystem.Provider), new(*Data)),
wire.Bind(new(datasystem.DatabaseProvider), new(*Data)),
+2 -2
View File
@@ -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 != 18 {
t.Fatalf("migration versions = %d, want 18", versions)
if versions != 19 {
t.Fatalf("migration versions = %d, want 19", versions)
}
var communicationRows []dataintegration.ConfigPO
if err = db.Where("kind IN ?", []string{"mq", "websocket"}).Order("kind, provider").Find(&communicationRows).Error; err != nil {