2026-06-04 18:31:06 +08:00

111 lines
3.6 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"context"
"strings"
"time"
"unicode/utf8"
"golang.org/x/crypto/bcrypt"
roomeventsv1 "hyapp.local/api/proto/events/room/v1"
roomv1 "hyapp.local/api/proto/room/v1"
"hyapp/pkg/xerr"
"hyapp/services/room-service/internal/room/command"
"hyapp/services/room-service/internal/room/outbox"
"hyapp/services/room-service/internal/room/rank"
"hyapp/services/room-service/internal/room/state"
)
const maxRoomPasswordRunes = 64
// SetRoomPassword 设置或清空房间入房密码。
// 密码属于 Room Cell 核心状态JoinRoom、快照恢复和列表投影都必须以同一份状态为准。
func (s *Service) SetRoomPassword(ctx context.Context, req *roomv1.SetRoomPasswordRequest) (*roomv1.SetRoomPasswordResponse, error) {
ctx = contextFromMeta(ctx, req.GetMeta())
password, passwordHash, err := normalizeRoomPassword(req.GetLocked(), req.GetPassword())
if err != nil {
return nil, err
}
cmd := command.SetRoomPassword{
Base: baseFromMeta(req.GetMeta()),
Locked: req.GetLocked(),
Password: password,
PasswordHash: passwordHash,
}
result, err := s.mutateRoom(ctx, cmd, true, func(now time.Time, current *state.RoomState, _ *rank.LocalRank) (mutationResult, []outbox.Record, error) {
if err := requireActiveRoom(current); err != nil {
return mutationResult{}, nil, err
}
if err := requireOwnerPresent(current, cmd.ActorUserID()); err != nil {
// 锁房只允许 owner 操作,避免普通房管把房主挡在自己的房间外。
return mutationResult{}, nil, err
}
if !cmd.Locked {
if current.RoomPasswordHash == "" {
// 重复解锁是 no-op只写 command log 保护幂等。
return mutationResult{snapshot: current.ToProto()}, nil, nil
}
current.RoomPasswordHash = ""
} else {
if roomPasswordMatches(current.RoomPasswordHash, password) {
// 同一个密码重复提交不产生版本和系统消息,避免客户端收到无意义状态变更。
return mutationResult{snapshot: current.ToProto()}, nil, nil
}
current.RoomPasswordHash = cmd.PasswordHash
}
current.Version++
currentHash := current.RoomPasswordHash
passwordEvent, err := outbox.Build(current.RoomID, "RoomPasswordChanged", current.Version, now, &roomeventsv1.RoomPasswordChanged{
ActorUserId: cmd.ActorUserID(),
Locked: cmd.Locked,
VisibleRegionId: current.VisibleRegionID,
})
if err != nil {
return mutationResult{}, nil, err
}
return mutationResult{
snapshot: current.ToProto(),
roomPasswordHash: &currentHash,
}, []outbox.Record{passwordEvent}, nil
})
if err != nil {
return nil, err
}
return &roomv1.SetRoomPasswordResponse{
Result: commandResult(result.applied, result.snapshot.GetVersion(), s.clock.Now()),
Room: result.snapshot,
}, nil
}
func normalizeRoomPassword(locked bool, raw string) (string, string, error) {
password := strings.TrimSpace(raw)
if !locked {
return "", "", nil
}
if password == "" {
return "", "", xerr.New(xerr.InvalidArgument, "room password is required")
}
if utf8.RuneCountInString(password) > maxRoomPasswordRunes {
// 密码只服务入房校验,必须给 snapshot 和 command log 一个有界载荷。
return "", "", xerr.New(xerr.InvalidArgument, "room password is too long")
}
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", "", err
}
return password, string(hash), nil
}
func roomPasswordMatches(hash string, password string) bool {
if hash == "" || password == "" {
return false
}
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
}