package auth import ( "context" "database/sql" "hyapp/pkg/appcode" "hyapp/pkg/xerr" authdomain "hyapp/services/user-service/internal/domain/auth" invitedomain "hyapp/services/user-service/internal/domain/invite" userdomain "hyapp/services/user-service/internal/domain/user" identitystorage "hyapp/services/user-service/internal/storage/mysql/identity" invitestorage "hyapp/services/user-service/internal/storage/mysql/invite" userstorage "hyapp/services/user-service/internal/storage/mysql/user" ) func (r *Repository) SetPassword(ctx context.Context, account authdomain.PasswordAccount) error { // password_accounts 以 user_id 唯一,v1 只允许首次设置密码。 _, err := r.db.ExecContext(ctx, ` INSERT INTO password_accounts (app_code, user_id, password_hash, hash_alg, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?) `, appcode.Normalize(account.AppCode), account.UserID, account.PasswordHash, account.HashAlg, account.CreatedAtMs, account.UpdatedAtMs) if err != nil { // 唯一键冲突映射为 PASSWORD_ALREADY_SET。 return mapAuthDuplicateError(err) } return nil } // CreatePasswordUser 原子创建完整密码账号,服务快捷建号和联调账号准备。 func (r *Repository) CreatePasswordUser(ctx context.Context, user userdomain.User, identity userdomain.Identity, account authdomain.PasswordAccount, session authdomain.Session, inviteBind invitedomain.BindCommand, maxAccountsPerDevice int32) error { // 普通快捷账号必须和 refresh session 同事务提交,避免返回不可登录的 uid; // 全站机器人会传入空 session,只写资料和密码身份,不产生任何可续期登录态。 tx, err := r.db.BeginTx(ctx, nil) if err != nil { return err } defer tx.Rollback() if err := ensureRegisterDeviceQuotaTx(ctx, tx, user.AppCode, user.RegisterDeviceID, maxAccountsPerDevice); err != nil { // 快捷建号也写 users.register_device_id;事务内锁定同设备行,避免绕过注册风控上限。 return err } if err := userstorage.InsertUserIdentity(ctx, tx, user, identity); err != nil { // 默认短号唯一键冲突交给 service 使用新 user_id/display_user_id 重试。 return mapAuthDuplicateError(err) } if _, err := invitestorage.EnsurePrimaryCodeForUser(ctx, tx, user.AppCode, user.UserID, user.CreatedAtMs); err != nil { return err } if _, err := invitestorage.BindRelation(ctx, tx, inviteBind); err != nil { return err } if _, err := tx.ExecContext(ctx, ` INSERT INTO password_accounts (app_code, user_id, password_hash, hash_alg, created_at_ms, updated_at_ms) VALUES (?, ?, ?, ?, ?, ?) `, appcode.Normalize(account.AppCode), account.UserID, account.PasswordHash, account.HashAlg, account.CreatedAtMs, account.UpdatedAtMs); err != nil { return mapAuthDuplicateError(err) } if session.SessionID != "" { if err := insertSession(ctx, tx, session); err != nil { // session 写入失败时必须回滚用户和密码身份,否则客户端拿不到 refresh token 却占用账号。 return err } } return tx.Commit() } // FindPasswordByDisplayUserID 通过当前 active 展示号或 held 默认短号登录别名查找用户密码身份。 func (r *Repository) FindPasswordByDisplayUserID(ctx context.Context, displayUserID string, nowMs int64) (authdomain.PasswordAccount, error) { // 密码登录前先按输入展示号触发靓号懒过期;输入 held 默认短号时也能恢复已到期靓号。 if err := identitystorage.ExpirePrettyByDisplayUserID(ctx, r.db, displayUserID, nowMs, "lazy_password_login"); err != nil { return authdomain.PasswordAccount{}, err } var account authdomain.PasswordAccount var displayUserIDKind string var displayUserIDExpiresAtMs int64 err := r.db.QueryRowContext(ctx, ` SELECT pa.app_code, pa.user_id, active_du.display_user_id, active_du.display_user_id_kind, COALESCE(active_du.expires_at_ms, 0), pa.password_hash, pa.hash_alg, pa.created_at_ms, pa.updated_at_ms FROM user_display_user_ids login_du INNER JOIN user_display_user_ids active_du ON active_du.app_code = login_du.app_code AND active_du.user_id = login_du.user_id AND active_du.status = ? AND (active_du.expires_at_ms IS NULL OR active_du.expires_at_ms = 0 OR active_du.expires_at_ms > ?) INNER JOIN password_accounts pa ON pa.app_code = login_du.app_code AND pa.user_id = login_du.user_id WHERE login_du.app_code = ? AND login_du.display_user_id = ? AND ( ( login_du.status = ? AND (login_du.expires_at_ms IS NULL OR login_du.expires_at_ms = 0 OR login_du.expires_at_ms > ?) ) OR ( login_du.status = ? AND login_du.display_user_id_kind = ? ) ) LIMIT 1 `, string(userdomain.DisplayUserIDStatusActive), nowMs, appcode.FromContext(ctx), displayUserID, string(userdomain.DisplayUserIDStatusActive), nowMs, string(userdomain.DisplayUserIDStatusHeld), string(userdomain.DisplayUserIDKindDefault)). Scan(&account.AppCode, &account.UserID, &account.DisplayUserID, &displayUserIDKind, &displayUserIDExpiresAtMs, &account.PasswordHash, &account.HashAlg, &account.CreatedAtMs, &account.UpdatedAtMs) if err == sql.ErrNoRows { // 短号不存在、处于 released/expired、没有对应 active 展示号或未设置密码都返回 not found,由 service 统一映射 AUTH_FAILED。 return authdomain.PasswordAccount{}, xerr.New(xerr.NotFound, "password account not found") } if err != nil { return authdomain.PasswordAccount{}, err } if err := r.repairPasswordLoginIdentity(ctx, account.AppCode, account.UserID, account.DisplayUserID, displayUserIDKind, displayUserIDExpiresAtMs, nowMs); err != nil { return authdomain.PasswordAccount{}, err } return account, nil } func (r *Repository) repairPasswordLoginIdentity(ctx context.Context, appCode string, userID int64, displayUserID string, displayUserIDKind string, expiresAtMs int64, nowMs int64) error { // 密码登录以 user_display_user_ids 的 active 行为准;同步 users 快照后,后续 freshUser 和 token 会携带同一个当前展示号。 _, err := r.db.ExecContext(ctx, ` UPDATE users SET current_display_user_id = ?, current_display_user_id_kind = ?, current_display_user_id_expires_at_ms = ?, updated_at_ms = GREATEST(updated_at_ms, ?) WHERE app_code = ? AND user_id = ? AND ( current_display_user_id <> ? OR current_display_user_id_kind <> ? OR COALESCE(current_display_user_id_expires_at_ms, 0) <> ? ) `, displayUserID, displayUserIDKind, nullableExpiresAt(expiresAtMs), nowMs, appcode.Normalize(appCode), userID, displayUserID, displayUserIDKind, expiresAtMs) return err } func nullableExpiresAt(value int64) any { if value <= 0 { return nil } return value } // CreateSession 创建新的 refresh session。