user

package
v1.8.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 4, 2026 License: Apache-2.0 Imports: 74 Imported by: 0

Documentation

Index

Constants

View Source
const (
	// Web3VerifyLogin 校验登录
	Web3VerifyLogin string = "login"
	// Web3VerifyPassword 校验密码
	Web3VerifyPassword string = "password"
)
View Source
const (
	// CategoryCustomerService 客服
	CategoryCustomerService = "customerService"
	// CategorySystem 系统账号
	CategorySystem = "system"
)
View Source
const (
	IsDestroyNo       = 0
	IsDestroyApplying = 1
	IsDestroyDone     = 2
)

注销状态:0=正常,1=冷静期(可撤销),2=已注销(最终)

View Source
const (
	BindReasonRateLimited      = "rate_limited"      // loginGuard 阈值已到,本次未走密码比对
	BindReasonUserNotFound     = "user_not_found"    // uid 不存在,前端应统一兜底"账号或密码错误"避免枚举
	BindReasonUserUnavailable  = "user_unavailable"  // 账号已注销或被封禁
	BindReasonPasswordMismatch = "password_mismatch" // 密码错误
)

VerifyPasswordByUID 返回的 reason 枚举。typed const 而非 magic string,既能 让调用方 switch-case 时被 govet 检出拼写错误,也作为"matched=false 时 reason 必非空"不变式的可枚举值集合。

View Source
const (
	WebhookUIDPrefix      = webhookUIDPrefix
	WebhookExtraAvatarKey = webhookExtraAvatarKey
)

导出别名仅供 incomingwebhook 包的测试做跨包契约一致性校验(见其 display_test.go), 把"本地复制的常量"与"上层源头常量"在编译期/测试期绑定,防止任一侧改动后悄悄漂移。 生产代码不依赖这些导出。

View Source
const (
	// CacheKeyFriends 好友key
	CacheKeyFriends string = "lm-friends:"
)
View Source
const (
	ChannelServiceName = "channel"
)
View Source
const LanguageCacheKeyPrefix = "user_language:"

LanguageCacheKeyPrefix is the Redis key prefix for the `user_language:{uid}` hot cache. Exported so external infra (e.g. ops scripts that need to invalidate a hot key) can construct the key without duplicating the literal.

View Source
const LanguageCacheTTL = 5 * time.Minute

LanguageCacheTTL bounds the staleness of cross-device language switches: after a PUT /v1/user/language the resolver writes through the cache, but if another node skipped that invalidation the next read converges within this window.

View Source
const RoleCacheKeyPrefix = "user_role:"

RoleCacheKeyPrefix is the Redis key prefix for the `user_role:{uid}` hot cache. Exported so ops scripts / role-mutation sites can invalidate the key without duplicating the literal.

View Source
const RoleCacheTTL = 60 * time.Second

RoleCacheTTL bounds how long a stale system role can survive after a privilege change. The token used to bake the role in for its full lifetime (days); resolving per request against this cache caps that staleness to the TTL while keeping the auth hot path off MySQL for the common case. Kept deliberately short because this gates admin / superAdmin access.

View Source
const (
	ThirdAuthcodePrefix = "thirdlogin:authcode:"
)
View Source
const (
	UserRedDotCategoryFriendApply = "friendApply"
)

Variables

View Source
var (
	ErrUserNeedVerification = errors.New("user need verification") // 用户需要验证
	// ErrUserDisabled / ErrUserDeviceInfoRequired are execLogin's client-facing
	// sentinels so every login entry point (main / OAuth / email / username) can
	// classify them uniformly: a disabled account is 403, a missing device info
	// is 400 — not a generic 500. errors.Is on these at the call site (see
	// respondExecLoginError) keeps the classification in one place.
	ErrUserDisabled           = errors.New("该用户已被禁用")
	ErrUserDeviceInfoRequired = errors.New("登录设备信息不能为空!")
)
View Source
var (
	ErrPinnedLimitExceeded = errors.New("置顶数量已达上限")
	ErrPinnedAlreadyExists = errors.New("该频道已置顶")
)

错误定义

View Source
var ErrDestroyStateConflict = errors.New("destroy state conflict")

ErrDestroyStateConflict 表示目标 uid 不在期望的注销状态:通常意味着并发请求已抢先改写。 调用方需以「业务冲突」而非「服务端错误」对外呈现。

View Source
var ErrExternalLoginNotConfigured = errors.New("user: external login handler not configured")

ErrExternalLoginNotConfigured 外部登录未注入 handler(通常是单测中未走 user.New 完整初始化)

View Source
var ErrLoginLocked = errors.New("登录失败次数过多,账号已被临时锁定,请稍后再试")

ErrLoginLocked 表示账号因连续登录失败被临时锁定。

View Source
var ErrOIDCBindNotConfigured = errors.New("user: oidc bind handler not configured")

ErrOIDCBindNotConfigured OIDC 自助绑定 handler 未注入(同 ErrExternalLoginNotConfigured 的故障模式:测试或部署时未完整走 user.New 初始化)。

View Source
var ErrUnsupportedLanguage = errors.New("user: language not in supported matrix")

ErrUnsupportedLanguage is returned by SetLanguage when the input is a well-formed but unsupported BCP 47 tag (i.e. not in the configured supported-language matrix). Callers can errors.Is-match this to choose a 4xx user-facing response, separate from infra errors (DB down, etc.) that should surface as generic 5xx-ish failures.

View Source
var ErrorUserNotExist = errors.New("用户不存在!")
View Source
var Names = []string{}/* 182 elements not displayed */

Names 注册用户随机名字

Functions

func CheckPassword

func CheckPassword(password, storedHash string) (matched bool, needsMigration bool)

CheckPassword 验证密码是否匹配存储的哈希值 同时支持 bcrypt 和旧版 MD5(MD5(password)) 格式 返回: matched(是否匹配), needsMigration(是否需要迁移到 bcrypt)

func HashPassword

func HashPassword(password string) (string, error)

HashPassword 使用 bcrypt 哈希密码

func InitGlobalPinnedDB

func InitGlobalPinnedDB(ctx *config.Context)

InitGlobalPinnedDB 初始化全局 PinnedDB(在 user 模块初始化时调用)

func QueryVerificationsByUIDs

func QueryVerificationsByUIDs(ctx *config.Context, uids []string) (map[string]*VerificationInfo, error)

QueryVerificationsByUIDs 批量查询 user_verification 表,返回 uid → *VerificationInfo。 无实名记录的 uid 不会出现在 map 里 —— 调用方把缺失视为未实名 (realname_verified=false),与 UserDetailResp / loginUserDetailResp 的 omitempty 语义一致。

实现走 verificationDB.QueryByUIDs(单次 `IN (?)` 查询),每批 1000 uid 以 verificationBatchSize 为上限避免超大群(成员上限 100000)一次性打爆 MySQL packet / planner。零 N+1:外部循环次数 = len(uids)/batch,与成员数线性相关, 每批一次 round trip,和 service.GetUserDetails 同模式。

func RegisterBotFriendApplyHook

func RegisterBotFriendApplyHook(hook BotFriendApplyHook)

RegisterBotFriendApplyHook 注册机器人好友申请通知回调

func RegisterGroupMemberChecker

func RegisterGroupMemberChecker(fn GroupMemberCheckFunc)

RegisterGroupMemberChecker 注册群成员检查函数(供 group 模块调用)

func RegisterGroupMemberExternalProvider

func RegisterGroupMemberExternalProvider(fn GroupMemberExternalProvider)

RegisterGroupMemberExternalProvider 注册群成员外部性字段提供者(供 group 模块调用)。 仅在 init 阶段调用一次,后续并发读安全由 RWMutex 提供。

func RemovePinnedForChannel

func RemovePinnedForChannel(channelID string, channelType uint8)

RemovePinnedForChannel 清理频道的所有置顶(供其他模块调用)

func RemovePinnedForUser

func RemovePinnedForUser(uid, channelID string, channelType uint8)

RemovePinnedForUser 清理用户在所有 Space 下指定频道的置顶(供其他模块调用) 注意:此方法会跨 Space 删除,仅用于全局性操作(如删好友)

func RemovePinnedForUserInSpace

func RemovePinnedForUserInSpace(uid, spaceID, channelID string, channelType uint8)

RemovePinnedForUserInSpace 清理用户在指定 Space 指定频道的置顶(供其他模块调用)

func ResolveWebhookDisplayName added in v1.6.2

func ResolveWebhookDisplayName(ctx *config.Context, uid string) (string, error)

ResolveWebhookDisplayName 返回合成发送者(如 incoming webhook 的 iwh_xxx,user 表里 没有对应行)的展示名,通过 BussDataSource.ChannelGet 注册链解析。离线推送链路用它兜底 渲染发送者名,避免 webhook 消息推送出来没有发件人名字。

返回 ("", nil) 表示没有任何模块处理该 uid(不是 webhook,或已删除);("", err) 仅在 datasource 真实故障时返回。调用方应把空名当作"无兜底",把 err 当作可记录的非致命错误。

func SendQRCodeInfo

func SendQRCodeInfo(uuid string, qrcode *common.QRCodeModel)

SendQRCodeInfo 发送二维码数据

func SetAppBotResolver

func SetAppBotResolver(fn AppBotResolverFunc)

SetAppBotResolver registers the App Bot identity resolver.

func ValidateName

func ValidateName(name string) error

ValidateName checks that a display name is non-blank and does not contain the @ character, which is used as delimiter in token cache entries (uid@name@role). Allowing @ in names would enable privilege escalation via role injection.

非空校验(需求模块3):去除空白、控制字符、零宽/格式字符后无可见内容则拒绝。 ValidateName 是昵称写入的统一守门(注册、改名、管理员建/改号都经过它), 第三方登录刻意绕开本函数,故此处加非空不影响第三方 nickname 为空的登录流程。

Types

type AddUserReq

type AddUserReq struct {
	Name     string
	UID      string // 如果无值,则随机生成
	Username string
	ShortNo  string // 如果无值,则自动生成
	Zone     string
	Phone    string
	Email    string
	Password string
	Robot    int // 机器人 0.否 1.是
}

AddUserReq AddUserReq

type AppBotResolverFunc

type AppBotResolverFunc func(uid string) string

AppBotResolverFunc resolves an App Bot UID to display name. Returns empty if not found.

type BlacklistModel

type BlacklistModel struct {
	UID      string // 用户唯一id
	Name     string // 用户名称
	Username string // 用户名
	db.BaseModel
}

BlacklistModel 黑名单用户

type BotFriendApplyHook

type BotFriendApplyHook func(applyUID, applyName, robotID, remark, token, spaceID string)

BotFriendApplyHook 机器人好友申请通知回调 botfather 模块注册这个回调来接收通知,避免循环依赖 spaceID: 申请来源 Space,用于隔离通知到正确的 Space

type Category

type Category string

type DB

type DB struct {
	// contains filtered or unexported fields
}

DB 用户db操作

func NewDB

func NewDB(ctx *config.Context) *DB

NewDB NewDB

func (*DB) AddOrRemoveBlacklistTx

func (d *DB) AddOrRemoveBlacklistTx(uid string, touid string, blacklist int, version int64, tx *dbr.Tx) error

AddOrRemoveBlacklist 添加黑名单

func (*DB) Blacklists

func (d *DB) Blacklists(uid string) ([]*BlacklistModel, error)

Blacklists 黑名单列表

func (*DB) Insert

func (d *DB) Insert(m *Model) error

Insert 添加用户

func (*DB) QueryByCategory

func (d *DB) QueryByCategory(category string) ([]*Model, error)

QueryByCategory 根据用户分类查询用户列表

func (*DB) QueryByEmail

func (d *DB) QueryByEmail(email string) (*Model, error)

QueryByEmail 通过邮箱查询用户信息

func (*DB) QueryByKeyword

func (d *DB) QueryByKeyword(keyword string) (*Model, error)

QueryByKeyword 通过用户名查询用户信息

func (*DB) QueryByPhone

func (d *DB) QueryByPhone(zone string, phone string) (*Model, error)

QueryByPhone 通过手机号和区号查询用户信息

func (*DB) QueryByPhones

func (d *DB) QueryByPhones(phones []string) ([]*Model, error)

查询多个手机号用户

func (*DB) QueryByUID

func (d *DB) QueryByUID(uid string) (*Model, error)

QueryByUID 通过用户uid查询用户信息

func (*DB) QueryByUIDs

func (d *DB) QueryByUIDs(uids []string) ([]*Model, error)

QueryByUIDs 根据用户uid查询用户信息

func (*DB) QueryByUsername

func (d *DB) QueryByUsername(username string) (*Model, error)

QueryByUsername 通过用户名查询用户信息 支持用户名、手机号(格式:zone-phone)、邮箱三种方式登录

func (*DB) QueryByUsernameCxt

func (d *DB) QueryByUsernameCxt(ctx context.Context, username string) (*Model, error)

QueryByUsernameCxt 通过用户名查询用户信息

func (*DB) QueryByVercode

func (d *DB) QueryByVercode(vercode string) (*Model, error)

QueryByVercode 通过用户vercode查询用户信息

func (*DB) QueryDetailByUID

func (d *DB) QueryDetailByUID(uid string, loginUID string) (*Detail, error)

QueryDetailByUID 查询用户详情

func (*DB) QueryDetailByUIDs

func (d *DB) QueryDetailByUIDs(uids []string, loginUID string) ([]*Detail, error)

QueryDetailByUIDs 查询用户详情集合

func (*DB) QueryLanguageByUID added in v1.5.0

func (d *DB) QueryLanguageByUID(uid string) (string, error)

QueryLanguageByUID returns the user's language preference column only. 用作 LanguageService 的真相源读取——只 SELECT 单列以保持 hot path 轻量, 避免 5min TTL 的 Redis 缓存 miss 把整行 Model 拉到内存。 (uid 不存在时返回 ("", nil);空串语义为"未显式设置"。)

func (*DB) QueryRoleByUID added in v1.7.0

func (d *DB) QueryRoleByUID(uid string) (string, error)

QueryRoleByUID returns the user's system role column only ("superAdmin" / "admin" / "" for a normal user). Used by RoleService as the authoritative source behind the per-request role resolver — only SELECTs the single column to keep the auth hot path light, mirroring QueryLanguageByUID. (uid 不存在时返回 ("", nil),语义即"无系统角色"。)

func (*DB) QueryUIDsByUsernames

func (d *DB) QueryUIDsByUsernames(usernames []string) ([]string, error)

QueryUIDsByUsernames 通过用户名查询用户uids

func (*DB) QueryUserWithOnlyShortNo

func (d *DB) QueryUserWithOnlyShortNo(shortNo string) (*Model, error)

QueryUserWithOnlyShortNo 通过short_no获取用户信息

func (*DB) QueryWithAppID

func (d *DB) QueryWithAppID(appID string) ([]*Model, error)

QueryWithAppID 根据appID查询用户列表

func (*DB) UpdateAvatarUploadStatus added in v1.6.0

func (d *DB) UpdateAvatarUploadStatus(uid string, avatarVersion int64) error

UpdateAvatarUploadStatus marks a user avatar as uploaded and stores the server-side object-key version used to avoid CDN query-string cache-key dependencies.

func (*DB) UpdateLanguageByUID added in v1.5.0

func (d *DB) UpdateLanguageByUID(uid, language string) error

UpdateLanguageByUID 持久化用户语言偏好。空字符串表示清空(回到 OCTO_DEFAULT_LANGUAGE 语义);调用方负责在入库前完成 BCP 47 校验 (通常走 LanguageService.SetLanguage),这里不做二次校验。

func (*DB) UpdateUsersWithField

func (d *DB) UpdateUsersWithField(field string, value string, uid string) error

UpdateUsersWithField 修改用户基本资料

func (*DB) UpdateUsersWithFieldTx

func (d *DB) UpdateUsersWithFieldTx(field string, value string, uid string, tx *dbr.Tx) error

UpdateUsersWithFieldTx 修改用户基本资料(事务版本)

type Detail

type Detail struct {
	Model
	Mute         int // 免打扰
	Top          int // 置顶
	ChatPwdOn    int //是否开启聊天密码
	Screenshot   int //截屏通知
	RevokeRemind int //撤回提醒
	Receipt      int //消息回执
	db.BaseModel
}

Detail 详情

type DetailModel

type DetailModel struct {
	Remark     string //好友备注
	ToUID      string // 好友uid
	ToName     string // 好友名字
	ToCategory string // 用户分类
	Mute       int    // 免打扰
	Top        int    // 置顶
	Version    int64  // 版本
	Vercode    string // 验证码 加好友需要
	IsDeleted  int    // 是否删除
	IsAlone    int    // 是否为单项好友
	ShortNo    string //短编号
	ChatPwdOn  int    // 是否开启聊天密码
	Blacklist  int    //是否在黑名单
	Receipt    int    //消息是否回执
	Robot      int    // 机器人0.否1.是
	db.BaseModel
}

DetailModel 好友详情

type DeviceInfo

type DeviceInfo struct {
	DeviceID    string
	DeviceName  string
	DeviceModel string
}

DeviceInfo 登录设备信息(外部模块用,与内部 deviceReq 解耦)

type ExternalLoginReq

type ExternalLoginReq struct {
	ExistingUID string

	// UID 仅新建用户场景下使用,ExistingUID 非空时忽略。
	UID string // 调用方生成的 UID(避免重复 GenerUUID 后还要再回传)

	// Name 在两条路径都用:新建时写入 user.name;ExistingUID 非空时与库中
	// user.name 比较,不同则同步覆盖(issue #1307)。两条路径都会做 @ → _ 消毒。
	Name  string
	Email string
	Phone string
	Zone  string

	DeviceFlag config.DeviceFlag
	Device     *DeviceInfo

	// PublicIP 用于欢迎消息日志,可空
	PublicIP string

	// TrustedSSOCreate 调用方声明本次新建用户的身份已由可信 IdP 完成认证,
	// 且已经过 oidc 模块的 IssuerAllowlist 准入校验(详见
	// modules/oidc/bind_service.go BindService.Create 与
	// modules/oidc/api.go callback `res.IsNew` 分支)。
	//
	// 置 true 时本方法**绕过** common.SystemSettings.RegisterOff() 全局开关:
	// register.off=1 主要用于阻断公开 email/手机号注册入口与 GitHub/Gitee OAuth
	// 自助建号通道,这两条通道的身份来源都是不受 dmwork 控制的外部输入。
	// OIDC 通道在本 PR 后由 DM_OIDC_PROVIDER_ALLOW_NEW_USER 与
	// OCTO_OIDC_BIND_ALLOW_CREATE 独立控制(IssuerAllowlist 兜底),
	// 语义上属于"可信 IdP 授权创建",不应受 register.off 影响。
	//
	// 仅 OIDC 模块在 callback `res.IsNew=true` / `/bind/create` 路径上置 true;
	// 其他外部 IdP(GitHub / Gitee 等)留 false,保留原有 register.off 守护。
	TrustedSSOCreate bool
}

ExternalLoginReq external IdP(OIDC / OAuth)登录入参。

ExistingUID 为空表示按 IdP 返回的 claims 新建本地用户;非空则按已知 UID 登录。 调用方(oidc 模块的 ResolveOrLink)负责完成 (issuer, sub) → uid 的解析与绑定, 这里只负责签发 DMWork 会话 token + 推 WuKongIM。

type ExternalLoginResp

type ExternalLoginResp struct {
	UID           string
	IsNewUser     bool
	LoginRespJSON string
}

ExternalLoginResp 外部登录结果。

LoginRespJSON 是 loginUserDetailResp 序列化后的 JSON 字符串,可直接落到 ThirdAuthcode Redis 缓冲区供前端短码轮询取走;调用方无需关心其内部结构。

type Friend

type Friend struct {
	log.Log
	// contains filtered or unexported fields
}

Friend 好友

func NewFriend

func NewFriend(ctx *config.Context) *Friend

NewFriend 创建

func (*Friend) Route

func (f *Friend) Route(r *wkhttp.WKHttp)

Route 配置路由规则

type FriendApplyModel

type FriendApplyModel struct {
	UID    string
	ToUID  string
	Remark string
	Token  string
	Status int // 状态 0.未处理 1.通过 2.拒绝
	db.BaseModel
}

FriendApplyModel 好友申请记录

type FriendDetailModel

type FriendDetailModel struct {
	FriendModel
	Name   string // 用户名称
	ToName string //对方用户名称
}

FriendDetailModel 好友资料

type FriendModel

type FriendModel struct {
	UID           string
	ToUID         string
	Flag          int
	Version       int64
	IsDeleted     int
	IsAlone       int // 是否为单项好友
	Vercode       string
	SourceVercode string //来源验证码
	Initiator     int    //1:发起方
	db.BaseModel
}

FriendModel 好友对象

type FriendReq

type FriendReq struct {
	UID     string
	ToUID   string
	Flag    int
	Version int64
}

FriendReq FriendReq

type FriendResp

type FriendResp struct {
	Name    string
	UID     string
	IsAlone int // 是否为单项好友
	Remark  string
}

FriendResp 用户好友

type GroupMemberCheckFunc

type GroupMemberCheckFunc func(groupNo string, uid string) (bool, error)

GroupMemberCheckFunc 检查用户是否为群成员的函数类型

type GroupMemberExternalProvider

type GroupMemberExternalProvider func(groupNo, uid string) (
	isExternal int,
	sourceSpaceID, sourceSpaceName string,
	homeSpaceID, homeSpaceName string,
	err error,
)

GroupMemberExternalProvider 返回群成员的外部来源/归属 Space 字段 (对齐 group 模块 /groups/{no}/members 的 memberDetailResp 命名)。

返回值:

  • isExternal: 1 表示外部成员;0 表示内部
  • sourceSpaceID / sourceSpaceName: 来源 Space(仅外部成员非空)
  • homeSpaceID / homeSpaceName: 相对视角归属 Space (外部 → source space;内部 → 群自身 space)
  • err: 底层查询失败

当 groupNo/uid 为空或成员不存在时,返回全零值和 nil error。 由 group 模块在 init 阶段通过 RegisterGroupMemberExternalProvider 注入, 避免 user 模块反向依赖 group 包,保留单模块编译、按需启用的能力。

type GroupMemberResp

type GroupMemberResp struct {
	UID                string `json:"uid"`                  // 成员uid
	GroupNo            string `json:"group_no"`             // 群唯一编号
	Name               string `json:"name"`                 // 群成员名称
	Remark             string `json:"remark"`               // 成员备注
	Role               int    `json:"role"`                 // 成员角色
	IsDeleted          int    `json:"is_deleted"`           // 是否删除
	Status             int    `json:"status"`               //成员状态0:正常,2:黑名单
	Vercode            string `json:"vercode"`              // 验证码
	InviteUID          string `json:"invite_uid"`           // 邀请人
	Robot              int    `json:"robot"`                // 机器人
	ForbiddenExpirTime int64  `json:"forbidden_expir_time"` // 禁言时长
	// YUJ-206:透传群成员的外部来源/归属 Space 字段,命名与 /groups/{no}/members
	// 的 memberDetailResp 保持一致,供 Web/Android/iOS UserInfo 判定"同 Space 非
	// 好友 → 直接发消息" vs "跨 Space 外部成员 → 仅可在群内交流"。
	// 后端保留 IsExternal / SourceSpaceID / SourceSpaceName 的绝对语义不变;
	// HomeSpaceID / HomeSpaceName 是对齐企微相对视角的视图字段:
	//   外部成员 (is_external == 1) → home_space_id = source_space_id
	//   内部成员                     → home_space_id = group.space_id
	IsExternal      int    `json:"is_external"`       // 是否外部成员 0/1
	SourceSpaceID   string `json:"source_space_id"`   // 来源 Space ID(外部成员使用)
	SourceSpaceName string `json:"source_space_name"` // 来源 Space 名称
	HomeSpaceID     string `json:"home_space_id"`     // 成员归属 Space ID(相对视角)
	HomeSpaceName   string `json:"home_space_name"`   // 成员归属 Space 名称
	CreatedAt       string `json:"created_at"`
	UpdatedAt       string `json:"updated_at"`
}

type IOnlineService

type IOnlineService interface {
	// 获取用户最新在线状态
	GetUserLastOnlineStatus(uids []string) ([]*config.OnlinestatusResp, error)

	// 判断用户设备是否在线
	DeviceOnline(uid string, device config.DeviceFlag) (bool, error)

	// 总在线人数
	GetOnlineCount() (int64, error)
}

type IService

type IService interface {
	//获取用户
	GetUser(uid string) (*Resp, error)
	// 获取用户详情(包括与loginUID的关系等等)
	GetUserDetail(uid string, loginUID string) (*UserDetailResp, error)
	// 批量获取用户详情。ctx 用于按请求协商语言渲染 BotFather 命令菜单(#335),
	// 无请求上下文的调用方传 context.Background() 即回退部署默认语言。
	GetUserDetails(ctx context.Context, uids []string, loginUID string) ([]*UserDetailResp, error)
	// 通过用户名获取用户
	GetUserWithUsername(username string) (*Resp, error)
	// 通过用户名获取用户uid集合
	GetUserUIDWithUsernames(usernames []string) ([]string, error)
	// 批量获取用户信息
	GetUsers(uids []string) ([]*Resp, error)
	// 通过APPID获取用户
	GetUsersWithAppID(appID string) ([]*Resp, error)
	// 获取用户集合
	GetUsersWithCategory(category Category) ([]*Resp, error)
	// 获取指定类别的用户列表
	GetUsersWithCategories(categories []string) ([]*Resp, error)
	//查询某个人好友
	GetFriendsWithToUIDs(uid string, toUIDs []string) ([]*FriendResp, error)
	//查询某个用户的所有好友
	GetFriends(uid string) ([]*FriendResp, error)
	//添加一个好友
	AddFriend(uid string, friend *FriendReq) error
	//添加一个用户
	AddUser(user *AddUserReq) error
	// 通过qrvercode获取用户信息
	GetUserWithQRVercode(qrVercode string) (*Resp, error)
	// 获取总用户数量
	GetAllUserCount() (int64, error)
	// 查询某天注册用户数
	GetRegisterWithDate(date string) (int64, error)
	// 获取某个时间区间的注册数量
	GetRegisterCountWithDateSpace(startDate, endDate string) (map[string]int64, error)
	// IsFriend 查询两个用户是否为好友关系
	IsFriend(uid string, toUID string) (bool, error)
	// 获取在线用户
	GetUserOnlineStatus([]string) ([]*OnLineUserResp, error)
	// 更新用户信息
	UpdateUser(req UserUpdateReq) error
	// 获取所有用户
	GetAllUsers() ([]*Resp, error)
	// 更新登录密码
	UpdateLoginPassword(req UpdateLoginPasswordReq) error

	// GetUserSettings 获取用户的配置
	GetUserSettings(uids []string, loginUID string) ([]*SettingResp, error)

	// GetOnetimePrekeyCount 获取用户一次性signal key的数量(决定是否可以开启加密通讯)
	GetOnetimePrekeyCount(uid string) (int, error)

	// 获取设备在线状态
	GetDeviceOnline(uid string, deviceFlag config.DeviceFlag) (*config.OnlinestatusResp, error)
	// 查询在线用户总数量
	GetOnlineCount() (int64, error)
	// 存在黑明单
	ExistBlacklist(uid string, toUID string) (bool, error)
	// QueryPeerRobotInfo 返回目标用户是否为 bot 及其创建者 UID。
	// 实现委托给 PinnedDB(user/db_pinned.go),与置顶频道访问校验共用同一 SQL 真源。
	// 用于 messages_search 等模块的 p2p 访问门禁区分本人 bot / 他人 bot / 真人。
	QueryPeerRobotInfo(peerUID string) (isRobot bool, creatorUID string, err error)
	// AreSpaceMembers 校验两个 uid 是否同属一个在籍 Space。
	// 实现委托给 PinnedDB(user/db_pinned.go),底层调用 pkg/space.CheckBothMembers。
	// 用于 Space 模式下放行同 Space 成员间的 p2p 交互(无需互加好友)。
	AreSpaceMembers(spaceID, uid1, uid2 string) (bool, error)
	// 更新用户消息过期时长
	UpdateUserMsgExpireSecond(uid string, msgExpireSecond int64) error
	// 搜索好友
	SearchFriendsWithKeyword(uid string, keyword string) ([]*FriendResp, error)

	// LoginByExternalIdentity 给外部 IdP(OIDC / OAuth)登录流程签发 DMWork 会话。
	//
	// 内部委托给 *User 实现,需要在模块初始化时通过 (*Service).SetExternalLoginHandler 注入。
	// 未注入或调用方为 *Service 以外的 IService 实现时,返回 ErrExternalLoginNotConfigured。
	LoginByExternalIdentity(ctx context.Context, req ExternalLoginReq) (*ExternalLoginResp, error)

	// UpsertVerificationFromOIDC 基于 Aegis OIDC identity_verification scope
	// 返回的 claims 写入 user_verification 表(YUJ-382 / Aegis OIDC Phase 1 直切)。
	//
	// 自 2026-05-10 起替代原先的 verify-service HMAC 回调链路;权威写入口从
	// /v1/internal/verification/complete 转移到 oidc callback(登录时即写)。
	//
	// 语义:幂等 upsert,冲突按 user_id 主键全字段覆盖。调用方(oidc callback)
	// 负责判断 IsVerified / LegalName 非空 — 本方法再做一次防御式校验,空则 no-op。
	// verifiedProvider 必须在 allowlist 白名单内(cas/wecom/feishu),否则返错不写。
	UpsertVerificationFromOIDC(ctx context.Context, uid string, claims OIDCVerificationClaims) error

	// VerifyPasswordByUID 给 OIDC 自助绑定流程做账号密码二次验证(需求 FR-3.1)。
	//
	// 与 username 登录路径同款的 bcrypt/MD5 兼容 + loginGuard 反爆破,但走独立
	// 计数维度 ("oidc-bind:"+uid),避免与登录失败计数互相串扰。已注销/封号账号
	// 视为不可用。
	//
	// 三种返回组合(任一其它组合都是 bug,调用方可断言):
	//   - (true,  "",        nil)   密码正确且账号可用 → 推进绑定流程
	//   - (false, BindReason*, nil) 业务拒绝 → 计入审计,前端按 reason 显示文案
	//                                **(false, "", nil) 视为非法返回**,调用方应当
	//                                以基础设施异常处理(等同 err != nil)
	//   - (false, "",        err)   基础设施异常 / 调用方 contract 违反(如空 uid)
	//                                → 前端走兜底页 + 运维告警,不计审计失败计数
	//
	// reason 取值见 BindReason* 常量。
	//
	// 内部委托给 *User 实现,需通过 (*Service).SetOIDCBindHandler 注入;
	// 未注入时返回 ErrOIDCBindNotConfigured。
	VerifyPasswordByUID(ctx context.Context, uid, password string) (matched bool, reason string, err error)

	// SendOIDCBindSMS 向给定手机号发送 OIDC 绑定 OTP(需求 FR-3.3)。
	//
	// **调用方(oidc 模块)负责保证 zone/phone 来自 OIDC claims 的
	// phone_number/phone_number_verified,不接受用户输入** —— 否则攻击者
	// 可用自己手机绑别人 sub。本方法仅做信道分发,不做来源校验。
	//
	// keyspace 隔离边界(踩坑勘误):
	//   - 验证码本身 (CacheKeySMSCode) 按 codeType 分桶 —— OIDC bind 流程的
	//     OTP 不会被其他流程的验证码覆盖,也不会覆盖别人;
	//   - **但底层 SMSService 的"sms_rate_limit:zone@phone"(1min 发送频率)、
	//     "sms_verify_lock:zone@phone"(10min 失败锁定)、"sms_verify_fail:..."
	//     三个 key 都不带 codeType,跨流程共享**。后果:
	//       a. 用户刚走过 register/forget-pwd SMS 流程,1min 内进 OIDC bind
	//          会被 "发送过于频繁" 挡住;
	//       b. OIDC bind 路径连续输错 3 次,该手机号其他 SMS 流程一并被
	//          锁 10min。
	//   - bind_token 维度的"OTP 发送 ≤ 3 次"(SR-2.1)由 oidc 模块的
	//     BindStore.IncrAndCheck 单独兜底,与本层无关。
	SendOIDCBindSMS(ctx context.Context, zone, phone string) error

	// VerifyOIDCBindSMS 与 SendOIDCBindSMS 对称,复用底层 SMSService.Verify 的
	// 锁定/重试限制。**注意 lock/failCount key 同样不带 codeType,跨流程共享**,
	// 详见 SendOIDCBindSMS 注释。
	VerifyOIDCBindSMS(ctx context.Context, zone, phone, code string) error

	// IsBindable 给 oidc bind Confirm 路径在 identity.Insert 之前再校验一次
	// uid 仍可绑定。
	//
	// 必要性:locator/VerifyPasswordByUID 都只在 verify 阶段过滤
	// (is_destroy + status<>0)。verify→confirm 之间有 5min 用户交互窗口,
	// 期间运维可能 disable / 用户可能自助 destroy。若 confirm 不复核就 Insert,
	// 会给一个已不可绑定的账号写入 user_oidc_identity 行;残留脏数据让该
	// 用户后续 OIDC 登录走 (issuer, sub) autolink 命中 → IssueSession 拒绝 →
	// 死循环登录失败,需要人工 DB 清理。
	//
	// 实务上 TOCTOU 窗口仍 ~毫秒级(本方法返 true 后到 identity.Insert 之间),
	// DB 层 uk_uid_issuer + 登录路径的 status 检查兜底,但本方法把窗口从
	// "用户交互级"压缩到"DB 单次 round-trip 级",符合纵深防御。
	//
	// 返回:
	//   - (true, nil):账号可绑定
	//   - (false, nil):账号不可绑定(已 destroy / 已停用 / 不存在),调用方按业务拒绝处理
	//   - (false, err):基础设施错误,调用方按 internal_error 兜底
	IsBindable(ctx context.Context, uid string) (bool, error)
}

IService 用户服务接口

func NewService

func NewService(ctx *config.Context) IService

NewService NewService

type LanguageService added in v1.5.0

type LanguageService struct {
	// contains filtered or unexported fields
}

LanguageService resolves the authoritative language preference for a user. It satisfies both pkg/auth.LanguageResolver (consumed by CacheTokenParser) and pkg/i18n.UserLanguageResolver (documentation interface for the i18n contract).

Lookup order: Redis `user_language:{uid}` → DB `user.language` → "". DB results are written back to Redis with a negative marker for empty values so subsequent requests for users without a preference don't hammer MySQL. SetLanguage handles cross-device invalidation by deleting the hot key on writes; absent a delete the entry expires within LanguageCacheTTL.

func NewLanguageService added in v1.5.0

func NewLanguageService(db languageReader, c cache.Cache) *LanguageService

NewLanguageService constructs a LanguageService with the canonical 5-minute hot cache TTL. cache and db must be non-nil; nil is a programmer error and panics on construction rather than silently degrading at request time.

func (*LanguageService) Resolve added in v1.5.0

func (s *LanguageService) Resolve(ctx context.Context, uid string) (string, error)

Resolve returns the user's language preference or "" if none is set. Errors are returned to the caller; pkg/auth.CacheTokenParser specifically keeps the token snapshot on resolver error so an outage doesn't 5xx authentication.

func (*LanguageService) SetLanguage added in v1.5.0

func (s *LanguageService) SetLanguage(ctx context.Context, uid, lang string) error

SetLanguage validates the incoming preference, persists it to the DB and invalidates the hot cache so the next read on any node observes the new value within one round-trip. The empty string is accepted and clears the preference (back to "use OCTO_DEFAULT_LANGUAGE" semantics).

The DB write is performed via the embedded reader's underlying *DB when available; tests that supply a stub reader without a writer get an explicit error rather than a silent drop.

type LoginGuard

type LoginGuard struct {
	// contains filtered or unexported fields
}

LoginGuard 为登录接口提供连续失败计数与临时锁定能力,防止暴力破解 / 撞库。

计数维度:按请求传入的 account(username / phone / email)归一化后作为 key, 未登录阶段无 uid,使用 account 能覆盖"用户不存在"的探测场景。

存储:Redis INCR + EXPIRE,首次失败时设置 TTL,TTL 过期后自动解锁。

func NewLoginGuard

func NewLoginGuard(r *redis.Conn, threshold int64, window time.Duration) *LoginGuard

NewLoginGuard 创建 LoginGuard。threshold <=0 或 window <=0 时使用默认值。

func (*LoginGuard) Check

func (g *LoginGuard) Check(account string) error

Check 若当前失败计数已达阈值,返回 ErrLoginLocked;否则返回 nil。 空 account 视为无效标识,直接放行(由上层业务字段校验兜底)。

故障语义:Redis 不可用时 fail-open(仅记一条 warn),避免单点故障造成全量登录瘫痪。 代价是 Redis 抖动期间短暂失去暴力破解防护,但全局 IP 限流仍在生效作为兜底。

func (*LoginGuard) RecordFailure

func (g *LoginGuard) RecordFailure(account string) error

RecordFailure 失败次数 +1,每次都重设 TTL。

为什么每次都 Expire:INCR+EXPIRE 非原子,若首次 Incr 成功而 Expire 失败,key 将永不过期 导致账号永久锁定。每次 Expire 保证即使前几次 Expire 失败,后续失败也会修复 TTL。 语义副作用:计数窗口从"固定窗口"变为"滑动窗口"(攻击者持续尝试时窗口会续期), 这对防暴力破解反而更严格,符合安全预期。

func (*LoginGuard) RecordFailureLogged

func (g *LoginGuard) RecordFailureLogged(account string)

RecordFailureLogged 包装 RecordFailure,失败时只 warn 不扩散错误,方便 handler 调用。

func (*LoginGuard) Reset

func (g *LoginGuard) Reset(account string) error

Reset 登录成功后清除失败计数。

func (*LoginGuard) ResetLogged

func (g *LoginGuard) ResetLogged(account string)

ResetLogged 包装 Reset,失败时只 warn。

type LoginLog

type LoginLog struct {
	log.Log
	// contains filtered or unexported fields
}

LoginLog 用户设置

func NewLoginLog

func NewLoginLog(ctx *config.Context) *LoginLog

NewLoginLog 创建

type LoginLogDB

type LoginLogDB struct {
	// contains filtered or unexported fields
}

LoginLogDB 登录日志DB

func NewLoginLogDB

func NewLoginLogDB(session *dbr.Session) *LoginLogDB

NewLoginLogDB NewDB

type LoginLogModel

type LoginLogModel struct {
	LoginIP string //登录IP
	UID     string
	db.BaseModel
}

LoginLogModel 登录日志

type Manager

type Manager struct {
	log.Log
	// contains filtered or unexported fields
}

Manager 用户管理

func NewManager

func NewManager(ctx *config.Context) *Manager

NewManager NewManager

func (*Manager) Route

func (m *Manager) Route(r *wkhttp.WKHttp)

Route 配置路由规则

type Model

type Model struct {
	AppID             string //app id
	UID               string // 用户唯一id
	Name              string // 用户名称
	Username          string // 用户名
	Email             string // email地址
	Password          string // 用户密码
	Category          string //用户分类
	Sex               int    //性别
	ShortNo           string //唯一短编号
	ShortStatus       int    //唯一短编号是否修改0.否1.是
	Zone              string //区号
	Phone             string //手机号
	ChatPwd           string //聊天密码
	LockScreenPwd     string // 锁屏密码
	LockAfterMinute   int    // 在几分钟后锁屏 0表示立即
	DeviceLock        int    //是否开启设备锁
	SearchByPhone     int    //是否可以通过手机号搜索0.否1.是
	SearchByShort     int    //是否可以通过短编号搜索0.否1.是
	NewMsgNotice      int    //新消息通知0.否1.是
	MsgShowDetail     int    //显示消息通知详情0.否1.是
	VoiceOn           int    //声音0.否1.是
	ShockOn           int    //震动0.否1.是
	OfflineProtection int    // 离线保护
	Version           int64
	Status            int          // 状态 0.禁用 1.启用
	Vercode           string       //验证码
	QRVercode         string       // 二维码验证码
	IsUploadAvatar    int          // 是否上传过头像0:未上传1:已上传
	AvatarVersion     int64        // 头像对象版本,0 表示旧版稳定路径
	Role              string       // 角色 admin/superAdmin
	Robot             int          // 机器人0.否1.是
	MuteOfApp         int          // app是否禁音(当pc登录的时候app可以设置禁音,当pc登录后有效)
	IsDestroy         int          // 注销状态 0.正常 1.注销申请中(冷静期) 2.已注销
	DestroyApplyAt    dbr.NullTime // 注销申请时间
	DestroyExpireAt   dbr.NullTime // 注销到期执行时间
	WXOpenid          string       // 微信openid
	WXUnionid         string       // 微信unionid
	GiteeUID          string       // gitee uid
	GithubUID         string       // github uid
	Web3PublicKey     string       // web3公钥
	MsgExpireSecond   int64        // 消息过期时长
	Language          string       // 用户语言偏好(BCP 47,空表示沿用 OCTO_DEFAULT_LANGUAGE)
	db.BaseModel
}

Model 用户db model

type OIDCVerificationClaims

type OIDCVerificationClaims struct {
	// Subject OIDC sub,作为 user_verification.source_sub 写入。
	Subject string
	// VerifiedProvider Aegis 返回的 provider 全名,如 "cas.example.com"。
	// 存库前 strip 到一级(cas),不在 allowlist 内会被拒写。
	VerifiedProvider string
	// VerifiedAt Unix 秒,0 视为"未提供实名完成时间",Upsert 拒绝写入。
	VerifiedAt int64
	// LegalName 实名姓名。空字符串视为未实名,Upsert 拒绝写入。
	LegalName string
	// LegalEmail 实名邮箱,允许空。
	LegalEmail string
}

OIDCVerificationClaims 从 OIDC id_token / userinfo claims 里摘出的实名字段子集。

在 user 包定义(而不是直接引用 oidc.IDTokenClaims),避免 user → oidc 反向 import 环 —— oidc 模块已经 import user。字段命名与 oidc 层一致,方便调用方直接拷贝。

type OnLineUserResp

type OnLineUserResp struct {
	UID         string
	LastOffline int
	Online      int
	DeviceFlag  uint8
}

type OnlineService

type OnlineService struct {
	log.Log
	// contains filtered or unexported fields
}

func NewOnlineService

func NewOnlineService(ctx *config.Context) *OnlineService

func (*OnlineService) DeviceOnline

func (o *OnlineService) DeviceOnline(uid string, device config.DeviceFlag) (bool, error)

func (*OnlineService) GetOnlineCount

func (o *OnlineService) GetOnlineCount() (int64, error)

func (*OnlineService) GetUserLastOnlineStatus

func (o *OnlineService) GetUserLastOnlineStatus(uids []string) ([]*config.OnlinestatusResp, error)

type Pinned

type Pinned struct {
	log.Log
	// contains filtered or unexported fields
}

Pinned 置顶频道 API

func NewPinned

func NewPinned(db *PinnedDB, friendDB *friendDB) *Pinned

NewPinned 创建 Pinned API

func (*Pinned) Add

func (p *Pinned) Add(c *wkhttp.Context)

Add 添加置顶频道 POST /v1/user/pinned

func (*Pinned) List

func (p *Pinned) List(c *wkhttp.Context)

List 获取置顶频道列表 GET /v1/user/pinned

func (*Pinned) Remove

func (p *Pinned) Remove(c *wkhttp.Context)

Remove 移除置顶频道 DELETE /v1/user/pinned?channel_id=xxx&channel_type=2

func (*Pinned) UpdateSort

func (p *Pinned) UpdateSort(c *wkhttp.Context)

UpdateSort 更新置顶排序 PUT /v1/user/pinned/sort

type PinnedChannelModel

type PinnedChannelModel struct {
	ID          int64     `db:"id"`
	UID         string    `db:"uid"`
	SpaceID     string    `db:"space_id"`
	ChannelID   string    `db:"channel_id"`
	ChannelType uint8     `db:"channel_type"`
	SortOrder   int       `db:"sort_order"`
	CreatedAt   time.Time `db:"created_at"`
}

PinnedChannelModel 用户置顶频道模型

type PinnedDB

type PinnedDB struct {
	log.Log
	// contains filtered or unexported fields
}

PinnedDB 置顶频道数据库操作

func NewPinnedDB

func NewPinnedDB(ctx *config.Context) *PinnedDB

NewPinnedDB 创建 PinnedDB

func (*PinnedDB) Add

func (d *PinnedDB) Add(uid, spaceID, channelID string, channelType uint8, maxLimit int) error

Add 添加置顶频道。

并发一致性:

  • 先 SELECT COUNT(*) ... FOR UPDATE 取当前读并在匹配范围上加 next-key lock, 串行化同一 (uid, space_id) 下的并发插入。REPEATABLE READ 下普通的 一致性读 COUNT 使用事务启动时的快照,看不到其他事务已提交的插入, 因此必须用 FOR UPDATE 保证上限检查的正确性。
  • 唯一索引 uk_user_space_channel 配合 INSERT IGNORE 检测重复。

func (*PinnedDB) AreSpaceMembers

func (d *PinnedDB) AreSpaceMembers(spaceID, uid1, uid2 string) (bool, error)

AreSpaceMembers 校验 uid1、uid2 是否都是指定 Space 的在籍成员。 用于私聊置顶时,允许同 Space 成员(即便未互加好友)之间互相置顶。

func (*PinnedDB) List

func (d *PinnedDB) List(uid, spaceID string) ([]*PinnedChannelModel, error)

List 获取用户置顶频道列表

func (*PinnedDB) QueryPeerRobotInfo

func (d *PinnedDB) QueryPeerRobotInfo(peerUID string) (isRobot bool, creatorUID string, err error)

QueryPeerRobotInfo 返回目标用户是否为机器人,以及(若为机器人)其创建者 UID。 用于私聊置顶时区分普通用户与 bot:非本人创建的 bot 必须是好友关系才允许对话 / 置顶。

边界:LEFT JOIN 条件限定 r.status=1,因此 user.robot=1 但 robot.status!=1 (已停用的 bot)的情况下,返回 isRobot=true, creatorUID=""。此时调用方的 creator 匹配必然失败,会 fallback 到 "请先添加该机器人为好友"——对已停用 bot 采用更严格的访问控制,符合与 modules/robot/event.go existRobot 一致的语义。

func (*PinnedDB) Remove

func (d *PinnedDB) Remove(uid, spaceID, channelID string, channelType uint8) error

Remove 移除置顶频道

func (*PinnedDB) RemoveByChannel

func (d *PinnedDB) RemoveByChannel(channelID string, channelType uint8) error

RemoveByChannel 根据频道删除所有用户的置顶(用于频道删除/群解散时清理)

func (*PinnedDB) RemoveByUIDAndChannel

func (d *PinnedDB) RemoveByUIDAndChannel(uid, channelID string, channelType uint8) error

RemoveByUIDAndChannel 删除用户在所有 Space 下指定频道的置顶(用于删好友时清理) 注意:此方法会跨 Space 删除,仅用于全局性操作(如删好友)

func (*PinnedDB) RemoveByUIDSpaceChannel

func (d *PinnedDB) RemoveByUIDSpaceChannel(uid, spaceID, channelID string, channelType uint8) error

RemoveByUIDSpaceChannel 删除用户在指定 Space 指定频道的置顶(用于退群时清理)

func (*PinnedDB) UpdateSort

func (d *PinnedDB) UpdateSort(uid, spaceID string, items []PinnedSortItem) error

UpdateSort 更新置顶排序。

行为说明:

  • 忽略客户端提交的 SortOrder 字段,按 items 数组顺序从 1 开始重新编号, 避免客户端伪造 sort_order 造成冲突或超出范围。
  • 校验所有提交的频道都已被当前用户在当前 Space 下置顶。

type PinnedSortError

type PinnedSortError struct {
	// contains filtered or unexported fields
}

PinnedSortError 表示 UpdateSort 请求参数校验失败,属于客户端错误。 handler 可以通过 errors.As 判断并直接把 message 透传给客户端, 以区分 DB/系统错误(走泛化的 "更新排序失败" 提示)。

func (*PinnedSortError) Error

func (e *PinnedSortError) Error() string

type PinnedSortItem

type PinnedSortItem struct {
	ChannelID   string `json:"channel_id"`
	ChannelType uint8  `json:"channel_type"`
}

PinnedSortItem 排序项。SortOrder 由服务端按数组顺序重新分配, 客户端不需要也不应该提交;因此结构体中不包含 SortOrder 字段。

type Resp

type Resp struct {
	UID             string
	Name            string
	Zone            string
	Phone           string
	Email           string
	Status          int // 用户状态 1 正常 2:黑名单 0 禁用
	IsUploadAvatar  int
	NewMsgNotice    int
	MsgShowDetail   int //显示消息通知详情0.否1.是
	MsgExpireSecond int64
	CreatedAt       int64 // 注册时间 10位时间戳
	IsDestroy       int   // 是否注销
	Robot           int   // 机器人0.否1.是
}

Resp 用户返回

type RoleService added in v1.7.0

type RoleService struct {
	// contains filtered or unexported fields
}

RoleService resolves the authoritative system role for a user. It satisfies pkg/auth.RoleResolver, consumed by CacheTokenParser so that a role baked into a token at login no longer outlives a demotion until token expiry.

Lookup order: Redis `user_role:{uid}` → DB `user.role` → "". DB results are written back to Redis (empty stored as a negative marker) so subsequent requests for normal users don't touch MySQL. The TTL alone bounds staleness; callers that demote/remove an admin should additionally invalidate the hot key (Invalidate) for immediate effect.

func NewRoleService added in v1.7.0

func NewRoleService(db roleReader, c cache.Cache) *RoleService

NewRoleService constructs a RoleService with the canonical TTL. cache and db must be non-nil; nil is a programmer error and panics on construction rather than silently degrading at request time.

func (*RoleService) Invalidate added in v1.7.0

func (s *RoleService) Invalidate(uid string)

Invalidate drops the hot-cache entry for a user so the next request re-reads the role from DB. Call after any mutation of user.role (e.g. removing an admin) to make the change take effect within one round-trip instead of waiting out RoleCacheTTL. Best-effort: a Redis error degrades to TTL-bounded staleness, not a failure.

func (*RoleService) ResolveRole added in v1.7.0

func (s *RoleService) ResolveRole(ctx context.Context, uid string) (string, error)

ResolveRole returns the user's current system role, or "" if none. Errors are surfaced to the caller; pkg/auth.CacheTokenParser keeps the token snapshot on resolver error so a cache/DB outage does not fail authentication.

type Service

type Service struct {
	log.Log
	// contains filtered or unexported fields
}

Service Service

func (*Service) AddFriend

func (s *Service) AddFriend(uid string, friend *FriendReq) error

AddFriend 添加一个好友(若已存在则恢复为有效状态)

func (*Service) AddUser

func (s *Service) AddUser(user *AddUserReq) error

AddUser AddUser

func (*Service) AreSpaceMembers added in v1.7.0

func (s *Service) AreSpaceMembers(spaceID, uid1, uid2 string) (bool, error)

AreSpaceMembers 委托 PinnedDB,详见 IService 注释。

func (*Service) ExistBlacklist

func (s *Service) ExistBlacklist(uid string, toUID string) (bool, error)

func (*Service) GetAllUserCount

func (s *Service) GetAllUserCount() (int64, error)

GetAllUserCount 获取总用户数量

func (*Service) GetAllUsers

func (s *Service) GetAllUsers() ([]*Resp, error)

获取所有用户

func (*Service) GetDeviceOnline

func (s *Service) GetDeviceOnline(uid string, deviceFlag config.DeviceFlag) (*config.OnlinestatusResp, error)

func (*Service) GetFriends

func (s *Service) GetFriends(uid string) ([]*FriendResp, error)

GetFriends 查询某个用户的所有好友

func (*Service) GetFriendsWithToUIDs

func (s *Service) GetFriendsWithToUIDs(uid string, toUIDs []string) ([]*FriendResp, error)

GetFriendsWithToUIDs 查询一批好友

func (*Service) GetOnetimePrekeyCount

func (s *Service) GetOnetimePrekeyCount(uid string) (int, error)

func (*Service) GetOnlineCount

func (s *Service) GetOnlineCount() (int64, error)

查询在线总数量

func (*Service) GetRegisterCountWithDateSpace

func (s *Service) GetRegisterCountWithDateSpace(startDate, endDate string) (map[string]int64, error)

GetRegisterCountWithDateSpace 获取某个时间区间的注册数量

func (*Service) GetRegisterWithDate

func (s *Service) GetRegisterWithDate(date string) (int64, error)

GetRegisterWithDate 查询某天的注册量

func (*Service) GetUser

func (s *Service) GetUser(uid string) (*Resp, error)

GetUser 获取用户

func (*Service) GetUserDetail

func (s *Service) GetUserDetail(uid string, loginUID string) (*UserDetailResp, error)

func (*Service) GetUserDetails

func (s *Service) GetUserDetails(ctx context.Context, uids []string, loginUID string) ([]*UserDetailResp, error)

func (*Service) GetUserOnlineStatus

func (s *Service) GetUserOnlineStatus(uids []string) ([]*OnLineUserResp, error)

GetUserOnlineStatus 查询在线用户

func (*Service) GetUserSettings

func (s *Service) GetUserSettings(uids []string, loginUID string) ([]*SettingResp, error)

func (*Service) GetUserUIDWithUsernames

func (s *Service) GetUserUIDWithUsernames(usernames []string) ([]string, error)

GetUserUIDWithUsernames 获取用户uid集合

func (*Service) GetUserWithQRVercode

func (s *Service) GetUserWithQRVercode(qrVercode string) (*Resp, error)

GetUserWithQRVercode 通过qrvercode获取用户信息

func (*Service) GetUserWithUsername

func (s *Service) GetUserWithUsername(username string) (*Resp, error)

GetUserWithUsername 获取用户

func (*Service) GetUsers

func (s *Service) GetUsers(uids []string) ([]*Resp, error)

GetUsers 批量获取用户

func (*Service) GetUsersWithAppID

func (s *Service) GetUsersWithAppID(appID string) ([]*Resp, error)

GetUsersWithAppID 通过appID获取用户集合

func (*Service) GetUsersWithCategories

func (s *Service) GetUsersWithCategories(categories []string) ([]*Resp, error)

GetUsersWithCategories 获取指定类别的用户列表

func (*Service) GetUsersWithCategory

func (s *Service) GetUsersWithCategory(category Category) ([]*Resp, error)

GetUsersWithCategory 获取用户列表

func (*Service) IsBindable added in v1.4.0

func (s *Service) IsBindable(ctx context.Context, uid string) (bool, error)

IsBindable 详见 IService 注释。

func (*Service) IsFriend

func (s *Service) IsFriend(uid string, toUID string) (bool, error)

IsFriend 查询两个用户是否为好友关系

func (*Service) LoginByExternalIdentity

func (s *Service) LoginByExternalIdentity(ctx context.Context, req ExternalLoginReq) (*ExternalLoginResp, error)

LoginByExternalIdentity 委托给已注入的 handler。未注入时返回 ErrExternalLoginNotConfigured。

func (*Service) QueryPeerRobotInfo added in v1.7.0

func (s *Service) QueryPeerRobotInfo(peerUID string) (bool, string, error)

QueryPeerRobotInfo 委托 PinnedDB,详见 IService 注释。

func (*Service) SearchFriendsWithKeyword

func (s *Service) SearchFriendsWithKeyword(uid string, keyword string) ([]*FriendResp, error)

搜索好友

func (*Service) SendOIDCBindSMS added in v1.4.0

func (s *Service) SendOIDCBindSMS(ctx context.Context, zone, phone string) error

SendOIDCBindSMS 详见 IService 注释。

func (*Service) SetExternalLoginHandler

func (s *Service) SetExternalLoginHandler(h externalLoginHandler)

SetExternalLoginHandler 注入外部 IdP 登录 handler(在 user.New 内部调用,生产路径下保证非空)

func (*Service) SetOIDCBindHandler added in v1.4.0

func (s *Service) SetOIDCBindHandler(h oidcBindHandler)

SetOIDCBindHandler 注入 OIDC 自助绑定 handler(在 user.New 内部调用, 生产路径下保证非空)。未注入时三个 OIDC 绑定方法返回 ErrOIDCBindNotConfigured。

func (*Service) UpdateLoginPassword

func (s *Service) UpdateLoginPassword(req UpdateLoginPasswordReq) error

func (*Service) UpdateUser

func (s *Service) UpdateUser(req UserUpdateReq) error

func (*Service) UpdateUserMsgExpireSecond

func (s *Service) UpdateUserMsgExpireSecond(uid string, msgExpireSecond int64) error

func (*Service) UpsertVerificationFromOIDC

func (s *Service) UpsertVerificationFromOIDC(ctx context.Context, uid string, claims OIDCVerificationClaims) error

UpsertVerificationFromOIDC 详见 IService 注释。

语义检查顺序:

  1. uid / VerifiedAt / LegalName 基本必填(防 oidc callback 上游误调)
  2. VerifiedProvider strip domain → allowlist 白名单
  3. 复用 verificationDB.Upsert(与 verify-service 写入同一 SQL 路径)

func (*Service) VerifyOIDCBindSMS added in v1.4.0

func (s *Service) VerifyOIDCBindSMS(ctx context.Context, zone, phone, code string) error

VerifyOIDCBindSMS 详见 IService 注释。

func (*Service) VerifyPasswordByUID added in v1.4.0

func (s *Service) VerifyPasswordByUID(ctx context.Context, uid, password string) (bool, string, error)

VerifyPasswordByUID 详见 IService 注释。

type Setting

type Setting struct {
	log.Log
	// contains filtered or unexported fields
}

Setting 用户设置

func NewSetting

func NewSetting(ctx *config.Context) *Setting

NewSetting 创建

type SettingDB

type SettingDB struct {
	// contains filtered or unexported fields
}

SettingDB 设置db

func NewSettingDB

func NewSettingDB(session *dbr.Session) *SettingDB

NewSettingDB NewDB

func (*SettingDB) InsertUserSettingModel

func (d *SettingDB) InsertUserSettingModel(setting *SettingModel) error

InsertUserSettingModel 插入用户设置

func (*SettingDB) InsertUserSettingModelTx

func (d *SettingDB) InsertUserSettingModelTx(setting *SettingModel, tx *dbr.Tx) error

InsertUserSettingModelTx 插入用户设置

func (*SettingDB) QueryTwoUserSettingModel

func (d *SettingDB) QueryTwoUserSettingModel(uid, loginUID string) ([]*SettingModel, error)

QueryTwoUserSettingModel 查询双方用户设置

func (*SettingDB) QueryUserSettingModel

func (d *SettingDB) QueryUserSettingModel(uid, loginUID string) (*SettingModel, error)

QueryUserSettingModel 查询用户设置

func (*SettingDB) QueryUserSettings

func (d *SettingDB) QueryUserSettings(uids []string, loginUID string) ([]*SettingModel, error)

func (*SettingDB) QueryWithUidsAndToUID

func (d *SettingDB) QueryWithUidsAndToUID(uids []string, toUID string) ([]*SettingModel, error)

func (*SettingDB) UpdateUserSettingModel

func (d *SettingDB) UpdateUserSettingModel(setting *SettingModel) error

UpdateUserSettingModel 更新用户设置

type SettingModel

type SettingModel struct {
	UID          string // 用户UID
	ToUID        string // 对方uid
	Mute         int    // 免打扰
	Top          int    // 置顶
	ChatPwdOn    int    // 是否开启聊天密码
	Screenshot   int    //截屏通知
	RevokeRemind int    //撤回提醒
	Blacklist    int    //黑名单
	Receipt      int    //消息是否回执
	Flame        int    // 是否开启阅后即焚
	FlameSecond  int    // 阅后即焚秒数
	Version      int64  // 版本
	Remark       string // 备注
	db.BaseModel
}

SettingModel 用户设置

type SettingResp

type SettingResp struct {
	ToUID        string // 用户UID
	UID          string // 用户UID
	Mute         int    // 免打扰
	Top          int    // 置顶
	ChatPwdOn    int    // 是否开启聊天密码
	Screenshot   int    //截屏通知
	RevokeRemind int    //撤回提醒
	Blacklist    int    //黑名单
	Receipt      int    //消息是否回执
	Version      int64  // 版本
}

type SpaceSettingDB added in v1.4.0

type SpaceSettingDB struct {
	// contains filtered or unexported fields
}

func NewSpaceSettingDB added in v1.4.0

func NewSpaceSettingDB(session *dbr.Session) *SpaceSettingDB

func (*SpaceSettingDB) InsertIgnoreSpaceSetting added in v1.4.0

func (d *SpaceSettingDB) InsertIgnoreSpaceSetting(uid, spaceID string) error

func (*SpaceSettingDB) QuerySpaceSetting added in v1.4.0

func (d *SpaceSettingDB) QuerySpaceSetting(uid, spaceID string) (*SpaceSettingModel, error)

func (*SpaceSettingDB) UpdateSpaceSetting added in v1.4.0

func (d *SpaceSettingDB) UpdateSpaceSetting(uid, spaceID string, fields map[string]interface{}) error

type SpaceSettingModel added in v1.4.0

type SpaceSettingModel struct {
	UID                      string `db:"uid"`
	SpaceID                  string `db:"space_id"`
	VoiceInputEnabled        int    `db:"voice_input_enabled"`
	VoiceFeedbackOn          int    `db:"voice_feedback_on"`
	VoiceFeedbackNoticeAcked int    `db:"voice_feedback_notice_acked"`
}

type Status

type Status int

Status 状态

const (
	// StatusDisable 禁用
	StatusDisable Status = iota
	// StatusEnable 启用
	StatusEnable
)

func (Status) Int

func (s Status) Int() int

Int Int

type UpdateLoginPasswordReq

type UpdateLoginPasswordReq struct {
	UID         string // 用户uid
	Password    string // 用户旧密码
	NewPassword string // 用户新密码
}

type User

type User struct {
	log.Log
	// contains filtered or unexported fields
}

User 用户相关API

func New

func New(ctx *config.Context) *User

New New

func (*User) GetFriendByVercode

func (u *User) GetFriendByVercode(vercode string) (*source.FriendModel, error)

GetFriendByVercode 通过vercode获取好友信息

func (*User) GetFriendByVercodes

func (u *User) GetFriendByVercodes(vercodes []string) ([]*source.FriendModel, error)

func (*User) GetUserByMailListVercode

func (u *User) GetUserByMailListVercode(vercode string) (*source.UserModel, error)

通过通讯录验证码获取用户信息

func (*User) GetUserByQRVercode

func (u *User) GetUserByQRVercode(qrvercode string) (*source.UserModel, error)

GetUserByQRVercode 通过二维码验证码获取用户信息

func (*User) GetUserByUID

func (u *User) GetUserByUID(uid string) (*source.UserModel, error)

GetUserByUID 通过UID获取用户信息

func (*User) GetUserByVercode

func (u *User) GetUserByVercode(vercode string) (*source.UserModel, error)

GetUserByVercode 通过vercode获取用户信息

func (*User) IsBindable added in v1.4.0

func (u *User) IsBindable(_ context.Context, uid string) (bool, error)

IsBindable 详见 IService 注释。

可绑定条件比 VerifyPasswordByUID 更严:locator (oidc.dbBindLocator + DB QueryUIDsByEmail/Phone) 已经用 `is_destroy=0 AND status<>0` 过滤,本方法 必须与 locator 完全一致 —— 否则"verify 时被 locator 拒,改用别的入口 绕到 confirm"或"verify 通过后进入冷静期"两种场景就会出现 verify/confirm 不一致。

比 VerifyPasswordByUID 的 `IsDestroyDone || Status == 0` 多排除一个状态: is_destroy=1(冷静期内可撤销注销)。理由:5min bind 窗口内被用户主动发起 注销的账号,不该允许新 OIDC 身份绑定上去。

不计入 loginGuard 失败计数:Confirm 阶段调用,uid 已在 verify 阶段通过 密码/OTP 校验,这里只是状态二次确认。如果计失败会让"管理员在用户绑定 中途 disable"演变成"该 uid 被错锁 15min",体验差且无安全收益。

func (*User) LoginByExternalIdentity

func (u *User) LoginByExternalIdentity(ctx context.Context, req ExternalLoginReq) (*ExternalLoginResp, error)

LoginByExternalIdentity 给外部 IdP(OIDC / OAuth)登录流程签发 DMWork 会话。

ExistingUID 非空 → 走 execLogin(已有用户); ExistingUID 为空 → 走 createUserWithRespAndTx(创建用户 + 登录,事务内)。

行为复用 GitHub 登录路径(api_github.go),oidc 模块通过 IService 间接调用。

func (*User) Route

func (u *User) Route(r *wkhttp.WKHttp)

Route 路由配置

func (*User) SendOIDCBindSMS added in v1.4.0

func (u *User) SendOIDCBindSMS(ctx context.Context, zone, phone string) error

SendOIDCBindSMS 详见 IService 注释(含跨流程 keyspace 共享勘误)。

**安全契约(必须读)**:本方法不做"phone 来自 OIDC claims 而非用户输入"的来源校验, 因为 oidc 模块在 callback 阶段已经拿着可信 claims 转发过来。**如果未来 增加新的调用方,必须在该调用方内部确保 zone/phone 不可被用户控制**,否则:

  • 攻击者可用自己手机号 + 任意 OIDC sub 发起绑定流程,把别人 sub 绑到 自己 dmwork 账号(违反 FR-3.3 / SR-4 反伪造手机号)
  • 攻击者可批量请求发短信制造账号枚举 / 短信轰炸

实务防护(分层互补):

  • **本层**:commonapi.SMSService 内置 1min 发送频率 + 10min 失败锁定, 但锁定 key 不带 codeType(详见 IService 注释),与 register/forget-pwd 等其他 SMS 流程共享 -> 提供"全局手机号粒度"的反滥用;
  • **oidc 模块 BindService 层**:OCTO_OIDC_BIND_ISSUER_ALLOWLIST 收口 + bind_token 单次消费 + Redis 5min TTL + BindStore.IncrAndCheck (bind_token 维度的 OTP 发送/校验/确认 counter, 对应 SR-2.1) -> 提供"OIDC bind 流程粒度"的反爆破;
  • 两层叠加,phone 输入来源校验是调用方契约,不在本层兜底。

func (*User) UserAvatar

func (u *User) UserAvatar(c *wkhttp.Context)

UserAvatar 用户头像

func (*User) VerifyOIDCBindSMS added in v1.4.0

func (u *User) VerifyOIDCBindSMS(ctx context.Context, zone, phone, code string) error

VerifyOIDCBindSMS 详见 IService 注释(含跨流程 keyspace 共享勘误)。

反爆破隔离边界:

  • 与 loginGuard 的密码反爆破计数器独立(后者用 "oidc-bind:"+uid 前缀, SMSService 用 zone@phone 前缀)—— 用户即便密码尝试已被限流,仍可走 短信路径,反之亦然;
  • 但底层 SMSService 的"3 次失败锁定 10 分钟"行为 lock key 不带 codeType, 与 register / forget-pwd / login_check_phone / destroy / email_login 等所有 SMS 流程共享 —— OIDC bind 路径错 3 次也会把该手机号其他 SMS 流程一并锁 10min(详见 IService.SendOIDCBindSMS 注释)。如需 codeType 维度的独立锁,P0 之后单独评估改造 commonapi.SMSService(影响 5 个调 用方,需 ops 决策)。

func (*User) VerifyPasswordByUID added in v1.4.0

func (u *User) VerifyPasswordByUID(_ context.Context, uid, password string) (bool, string, error)

VerifyPasswordByUID 详见 IService 注释。

失败维度按 uid 计:配合 SR-2.2 "每 uid 当日 ≤ 10 次失败"。当前用与登录共用 的 loginGuard 默认阈值(5 次/15min),P0 上线后视实际反爆破效果再调 oidcBindGuardPrefix 维度的独立阈值。

已注销/封号账号视为不可用,与 api_usernamelogin.go 行为对齐 —— 攻击者用 OIDC bind 探测一个已注销账号的密码与登录路径同样无意义。

type UserDetailResp

type UserDetailResp struct {
	UID                 string            `json:"uid"`
	Name                string            `json:"name"`
	Username            string            `json:"username"`
	Email               string            `json:"email,omitempty"`              // email(仅自己能看)
	Zone                string            `json:"zone,omitempty"`               // 手机区号(仅自己能看)
	Phone               string            `json:"phone,omitempty"`              // 手机号(仅自己能看)
	Mute                int               `json:"mute"`                         // 免打扰
	Top                 int               `json:"top"`                          // 置顶
	Sex                 int               `json:"sex"`                          //性别1:男
	Category            string            `json:"category"`                     //用户分类 '客服'
	ShortNo             string            `json:"short_no"`                     // 用户唯一短编号
	ChatPwdOn           int               `json:"chat_pwd_on"`                  //是否开启聊天密码
	Screenshot          int               `json:"screenshot"`                   //截屏通知
	RevokeRemind        int               `json:"revoke_remind"`                //撤回提醒
	Receipt             int               `json:"receipt"`                      //消息是否回执
	Online              int               `json:"online"`                       //是否在线
	LastOffline         int               `json:"last_offline"`                 //最后一次离线时间
	DeviceFlag          config.DeviceFlag `json:"device_flag"`                  // 在线设备标记
	Follow              int               `json:"follow"`                       //是否是好友
	BeDeleted           int               `json:"be_deleted"`                   // 被删除
	BeBlacklist         int               `json:"be_blacklist"`                 // 被拉黑
	Code                string            `json:"code"`                         //加好友所需vercode TODO: code不再使用 请使用Vercode
	Vercode             string            `json:"vercode"`                      //
	SourceDesc          string            `json:"source_desc"`                  // 好友来源
	Remark              string            `json:"remark"`                       //好友备注
	IsUploadAvatar      int               `json:"is_upload_avatar"`             // 是否上传头像
	Status              int               `json:"status"`                       //用户状态 1 正常 2:黑名单
	Robot               int               `json:"robot"`                        // 机器人0.否1.是
	BotCommands         string            `json:"bot_commands,omitempty"`       // 机器人命令列表JSON
	BotDescription      string            `json:"bot_description,omitempty"`    // Bot 简介
	BotCreatorUID       string            `json:"bot_creator_uid,omitempty"`    // Bot 创建者 UID
	BotCreatorName      string            `json:"bot_creator_name,omitempty"`   // Bot 创建者昵称
	BotAutoApprove      int               `json:"bot_auto_approve,omitempty"`   // 是否自动通过好友 0:否 1:是
	BotAgentPlatform    string            `json:"bot_agent_platform,omitempty"` // Agent 平台名称
	BotAgentVersion     string            `json:"bot_agent_version,omitempty"`  // Agent 平台版本号
	BotPluginVersion    string            `json:"bot_plugin_version,omitempty"` // DMWork 插件版本号
	IsDestroy           int               `json:"is_destroy"`                   // 注销状态 0.正常 1.注销申请中(冷静期,仍可正常通信) 2.已注销
	Flame               int               `json:"flame"`                        // 是否开启阅后即焚
	FlameSecond         int               `json:"flame_second"`                 // 阅后即焚秒数
	JoinGroupInviteUID  string            `json:"join_group_invite_uid"`        // 加入群聊邀请人UID
	JoinGroupInviteName string            `json:"join_group_invite_name"`       // 加入群聊邀请人名称
	JoinGroupTime       string            `json:"join_group_time"`              // 加入群聊时间
	GroupMember         *GroupMemberResp  `json:"group_member,omitempty"`       // 群成员信息
	// OCTO 实名认证(YUJ-354 / GH#1300)
	// RealnameVerified:该用户是否已完成 OCTO 实名认证(user_verification 有记录)。
	// RealName:已认证时返回实名姓名;未认证留空。
	// 目前对外可见(含非自己),因为实名结果会进入前端 displayName() 展示;
	// 若后续产品要求"仅自己可见",改为仅当 loginUID == m.UID 时填充即可。
	RealnameVerified bool   `json:"realname_verified"`
	RealName         string `json:"real_name,omitempty"`
	// RealnameVerifiedAt:实名认证完成时间 (Unix 秒)。未认证时为 0 被 omitempty 剥离。
	// 三端客户端(Web/Android/iOS)在 Custom Tabs 实名回跳后读此字段做
	// 「已认证 · YYYY-MM」展示;同时与 login / GET /v1/user/current 下发的
	// 字段名保持一致(YUJ-413)。
	RealnameVerifiedAt int64 `json:"realname_verified_at,omitempty"`
}

func NewUserDetailResp

func NewUserDetailResp(m *Detail, remark, loginUID string, sourceFrom string, onLine int, lastOffline int, deviceFlag config.DeviceFlag, follow int, status int, beDeleted int, beBlacklist int, setting *SettingModel, vercode string) *UserDetailResp

type UserUpdateReq

type UserUpdateReq struct {
	UID  string
	Name *string
}

type VerificationInfo

type VerificationInfo struct {
	UID                string
	RealName           string
	RealnameVerifiedAt int64 // Unix 秒;未实名 / 时间零值时为 0
}

VerificationInfo 是对外导出的只读实名信息视图 —— 给其他模块(如 modules/group) 批量回填 response 用。YUJ-413 Scope B 引入:根因报告(YUJ-411 memory 07c6d080)发现 memberDetailResp 和 newChannelRespWithUserDetailResp 都漏下发 实名字段,这些调用方都不应再私有化 verificationDB 结构体,改走本视图。

字段对齐 login / current / friend-sync / conversation-sync 的 wire 协议:

realname_verified    ← (info != nil)
real_name            ← info.RealName
realname_verified_at ← info.RealnameVerifiedAt (Unix 秒)

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL