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: ¤tHash, }, []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 }