refactor(core): align architecture with cordis spatiotemporal composability

This commit is contained in:
ryan
2026-08-28 13:28:58 +08:00
parent 9f8890d159
commit e19bf36580
23 changed files with 1469 additions and 374 deletions
+1 -1
View File
@@ -81,7 +81,7 @@ func (p *Plugin) Apply(ctx *core.Context) error {
// 2. Initialize and provide UserService
if p.userSvc == nil {
p.userSvc = newUserService()
p.userSvc = newUserService(ctx.Events())
}
core.Provide[contracts.UserService](ctx, p.userSvc)
+257 -7
View File
@@ -11,13 +11,17 @@ import (
"strings"
"time"
"Wavelet/core"
"Wavelet/core/contracts"
"Wavelet/pkg/idgen"
database "Wavelet/plugins/infra/database"
"gorm.io/gorm"
pkgu "Wavelet/pkg/util"
)
const columnUpdatedAt = "updated_at"
func toUserDTO(u *User) *contracts.UserDTO {
if u == nil {
return nil
@@ -41,10 +45,16 @@ func toUserDTO(u *User) *contracts.UserDTO {
}
}
type userServiceImpl struct{}
type userServiceImpl struct {
events *core.EventBus
}
func newUserService() contracts.UserService {
return &userServiceImpl{}
func newUserService(events ...*core.EventBus) contracts.UserService {
var bus *core.EventBus
if len(events) > 0 {
bus = events[0]
}
return &userServiceImpl{events: bus}
}
func (s *userServiceImpl) GetUserByID(ctx context.Context, id uint64) (*contracts.UserDTO, error) {
@@ -131,7 +141,7 @@ func (s *userServiceImpl) UpdateProfile(ctx context.Context, id uint64, req cont
if req.Location != nil {
updates["location"] = *req.Location
}
updates["updated_at"] = time.Now()
updates[columnUpdatedAt] = time.Now()
if err := database.DB(ctx).Model(&User{}).Where("id = ?", id).Updates(updates).Error; err != nil {
return nil, err
@@ -156,8 +166,8 @@ func (s *userServiceImpl) UpdatePassword(ctx context.Context, id uint64, oldPass
return database.DB(ctx).Model(&User{}).Where("id = ?", id).
Updates(map[string]any{
"password": user.Password,
"updated_at": time.Now(),
"password": user.Password,
columnUpdatedAt: time.Now(),
}).Error
}
@@ -174,7 +184,7 @@ func (s *userServiceImpl) UpdateLastLogin(ctx context.Context, id uint64, _ stri
return database.DB(ctx).Model(&User{}).Where("id = ?", id).
Updates(map[string]any{
"last_login_at": time.Now(),
"updated_at": time.Now(),
columnUpdatedAt: time.Now(),
}).Error
}
@@ -280,3 +290,243 @@ func (s *userServiceImpl) UniqueUsername(ctx context.Context, base string) (stri
return "", errors.New("failed to generate unique username")
}
func (s *userServiceImpl) AdminListUsers(ctx context.Context, filter contracts.AdminListUsersFilter) (int64, []*contracts.UserDTO, error) {
query := database.DB(ctx).Table("w_users")
if filter.UserID != nil {
query = query.Where("id = ?", *filter.UserID)
}
if filter.Username != "" {
query = query.Where("username LIKE ? ESCAPE '\\'", pkgu.EscapeLike(filter.Username)+"%")
}
if filter.Email != "" {
query = query.Where("email LIKE ? ESCAPE '\\'", pkgu.EscapeLike(filter.Email)+"%")
}
var total int64
if err := query.Count(&total).Error; err != nil {
return 0, nil, err
}
if filter.Page <= 0 {
filter.Page = 1
}
if filter.PageSize <= 0 {
filter.PageSize = 20
}
var users []*contracts.UserDTO
offset := (filter.Page - 1) * filter.PageSize
if err := query.
Select("id, username, nickname, email, avatar_url, is_active, is_admin, bio, phone, gender, website, location, last_login_at, created_at, updated_at").
Order("id ASC").
Offset(offset).
Limit(filter.PageSize).
Find(&users).Error; err != nil {
return 0, nil, err
}
return total, users, nil
}
func (s *userServiceImpl) AdminGetUser(ctx context.Context, id uint64) (*contracts.UserDTO, error) {
var user contracts.UserDTO
if err := database.DB(ctx).Table("w_users").
Select("id, username, nickname, email, avatar_url, is_active, is_admin, bio, phone, gender, website, location, last_login_at, created_at, updated_at").
Where("id = ?", id).
First(&user).Error; err != nil {
return nil, err
}
return &user, nil
}
func (s *userServiceImpl) AdminCreateUser(ctx context.Context, req contracts.AdminCreateUserRequest) (*contracts.UserDTO, error) {
req.Username = strings.TrimSpace(req.Username)
req.Nickname = strings.TrimSpace(req.Nickname)
req.Password = strings.TrimSpace(req.Password)
req.Email = strings.TrimSpace(req.Email)
if req.Username == "" {
return nil, errors.New("用户名不能为空")
}
if req.Email == "" {
return nil, errors.New("邮箱不能为空")
}
const minPasswordLen = 8
if len(req.Password) < minPasswordLen {
return nil, errors.New("密码长度至少为 8 位")
}
var count int64
if err := database.DB(ctx).Table("w_users").Where("username = ?", req.Username).Count(&count).Error; err != nil {
return nil, err
}
if count > 0 {
return nil, errors.New("用户名已被使用")
}
var emailCount int64
if err := database.DB(ctx).Table("w_users").Where("email = ?", req.Email).Count(&emailCount).Error; err != nil {
return nil, err
}
if emailCount > 0 {
return nil, errors.New("邮箱已被使用")
}
hash, err := pkgu.HashPassword(req.Password)
if err != nil {
return nil, err
}
if req.Nickname == "" {
req.Nickname = req.Username
}
now := time.Now()
newUser := contracts.UserDTO{
ID: idgen.NextUint64ID(),
Username: req.Username,
Nickname: req.Nickname,
Email: req.Email,
IsActive: req.IsActive,
IsAdmin: req.IsAdmin,
CreatedAt: now,
UpdatedAt: now,
}
row := map[string]any{
"id": newUser.ID,
"username": newUser.Username,
"password": hash,
"nickname": newUser.Nickname,
"email": newUser.Email,
"is_active": newUser.IsActive,
"is_admin": newUser.IsAdmin,
"created_at": now,
columnUpdatedAt: now,
}
if err := database.DB(ctx).Table("w_users").Create(row).Error; err != nil {
return nil, err
}
if s.events != nil {
_ = s.events.Emit(ctx, contracts.EventTopicUserCreated, contracts.UserCreatedEvent{
User: &newUser,
Password: req.Password,
})
}
return &newUser, nil
}
func (s *userServiceImpl) AdminUpdateUser(ctx context.Context, currentUserID uint64, req contracts.AdminUpdateUserRequest) error {
req.Nickname = strings.TrimSpace(req.Nickname)
req.Email = strings.TrimSpace(req.Email)
req.Password = strings.TrimSpace(req.Password)
if req.Email == "" {
return errors.New("邮箱不能为空")
}
var targetUser contracts.UserDTO
if err := database.DB(ctx).Table("w_users").Where("id = ?", req.ID).First(&targetUser).Error; err != nil {
return err
}
if currentUserID == req.ID && !req.IsAdmin && targetUser.IsAdmin {
return errors.New("不能取消自己的管理员权限")
}
if targetUser.Email != req.Email {
var count int64
if err := database.DB(ctx).Table("w_users").Where("email = ? AND id != ?", req.Email, req.ID).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return errors.New("邮箱已被使用")
}
}
const minPasswordLen = 8
if req.Password != "" && len(req.Password) < minPasswordLen {
return errors.New("密码长度至少为 8 位")
}
if req.Nickname == "" {
req.Nickname = targetUser.Username
}
updates := map[string]any{
"nickname": req.Nickname,
"email": req.Email,
"is_admin": req.IsAdmin,
columnUpdatedAt: time.Now(),
}
if req.Password != "" {
hash, err := pkgu.HashPassword(req.Password)
if err != nil {
return err
}
updates["password"] = hash
}
err := database.DB(ctx).Table("w_users").Where("id = ?", req.ID).Updates(updates).Error
if err == nil && s.events != nil {
_ = s.events.Emit(ctx, contracts.EventTopicUserUpdated, &targetUser)
}
return err
}
func (s *userServiceImpl) AdminUpdateUserStatus(ctx context.Context, id uint64, active bool) error {
var flags struct {
ID uint64
IsAdmin bool
}
if err := database.DB(ctx).Table("w_users").Select("id, is_admin").Where("id = ?", id).First(&flags).Error; err != nil {
return err
}
if !active && flags.IsAdmin {
return errors.New("管理员账号无法被禁用")
}
err := database.DB(ctx).Table("w_users").Where("id = ?", id).Update("is_active", active).Error
if err == nil && s.events != nil {
_ = s.events.Emit(ctx, contracts.EventTopicUserStatusChanged, contracts.UserStatusChangedEvent{
UserID: id,
IsActive: active,
})
}
return err
}
func (s *userServiceImpl) AdminDeleteUser(ctx context.Context, currentUserID, targetID uint64) error {
if currentUserID == targetID {
return errors.New("不能删除当前登录用户")
}
var flags struct {
ID uint64
IsAdmin bool
}
if err := database.DB(ctx).Table("w_users").Select("id, is_admin").Where("id = ?", targetID).First(&flags).Error; err != nil {
return err
}
if flags.IsAdmin {
return errors.New("管理员账号无法被删除")
}
err := database.DB(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Table("w_access_tokens").Where("user_id = ?", targetID).Delete(map[string]any{}).Error; err != nil {
return err
}
if err := tx.Table("w_external_accounts").Where("user_id = ?", targetID).Delete(map[string]any{}).Error; err != nil {
return err
}
return tx.Table("w_users").Where("id = ?", targetID).Delete(map[string]any{}).Error
})
if err == nil && s.events != nil {
_ = s.events.Emit(ctx, contracts.EventTopicUserDeleted, contracts.UserDeletedEvent{
CurrentUserID: currentUserID,
TargetUserID: targetID,
})
}
return err
}