package host import ( "context" "database/sql" "fmt" "strconv" "strings" "hyapp/pkg/appcode" "hyapp/pkg/xerr" hostdomain "hyapp/services/user-service/internal/domain/host" hostservice "hyapp/services/user-service/internal/service/host" ) // SearchAgencies 返回用户当前国家内可加入的有效 Agency。 func (r *Repository) SearchAgencies(ctx context.Context, command hostservice.SearchAgenciesCommand) ([]hostdomain.Agency, error) { country, err := r.userCountry(ctx, r.db, command.UserID, "") if err != nil { return nil, err } if country == "" { // 国家是 App 推荐列表的硬边界,缺失时不能退化成跨国或全局列表。 return nil, xerr.New(xerr.InvalidArgument, "user country is required") } query := ` SELECT a.agency_id, a.owner_user_id, COALESCE(owner.region_id, 0), a.parent_bd_user_id, a.name, a.avatar, a.status, a.join_enabled, a.max_hosts, a.created_by_user_id, a.created_at_ms, a.updated_at_ms, COUNT(active_member.membership_id) FROM agencies a INNER JOIN users owner ON owner.app_code = a.app_code AND owner.user_id = a.owner_user_id LEFT JOIN agency_memberships active_member ON active_member.app_code = a.app_code AND active_member.agency_id = a.agency_id AND active_member.status = ? WHERE a.app_code = ? AND owner.country = ? AND owner.status = 'active' AND a.status = ? AND a.join_enabled = TRUE ` args := []any{hostdomain.MembershipStatusActive, appcode.FromContext(ctx), country, hostdomain.AgencyStatusActive} if strings.TrimSpace(command.Keyword) != "" { keyword := strings.TrimSpace(command.Keyword) query += ` AND (a.name LIKE ? OR a.agency_id = ? OR owner.current_display_user_id = ?)` args = append(args, "%"+keyword+"%", int64FromKeyword(keyword), keyword) } query += ` GROUP BY a.agency_id, a.owner_user_id, owner.region_id, a.parent_bd_user_id, a.name, a.avatar, a.status, a.join_enabled, a.max_hosts, a.created_by_user_id, a.created_at_ms, a.updated_at_ms ORDER BY a.created_at_ms DESC, a.agency_id DESC LIMIT ? ` args = append(args, command.PageSize) rows, err := r.db.QueryContext(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() agencies := make([]hostdomain.Agency, 0, command.PageSize) for rows.Next() { agency, err := scanSearchAgency(rows) if err != nil { return nil, err } agencies = append(agencies, agency) } return agencies, rows.Err() } // GetHostProfile 读取 Host 身份事实。 func (r *Repository) GetHostProfile(ctx context.Context, userID int64) (hostdomain.HostProfile, error) { return queryHostProfile(ctx, r.db, "WHERE user_id = ?", userID) } // BatchGetHostProfiles 批量读取 Host 身份事实,缺失 user_id 不填充空 profile。 func (r *Repository) BatchGetHostProfiles(ctx context.Context, userIDs []int64) (map[int64]hostdomain.HostProfile, error) { if len(userIDs) == 0 { return map[int64]hostdomain.HostProfile{}, nil } placeholders := make([]string, 0, len(userIDs)) args := make([]any, 0, len(userIDs)+1) args = append(args, appcode.FromContext(ctx)) for _, userID := range userIDs { placeholders = append(placeholders, "?") args = append(args, userID) } rows, err := r.db.QueryContext(ctx, fmt.Sprintf(` SELECT %s FROM host_profiles WHERE app_code = ? AND user_id IN (%s) `, hostProfileColumns, strings.Join(placeholders, ",")), args...) if err != nil { return nil, err } defer rows.Close() result := make(map[int64]hostdomain.HostProfile, len(userIDs)) for rows.Next() { // 返回 map 以 user_id 为键,gateway 可以精确区分“非主播/缺失”和“主播但状态不可入账”。 profile, err := scanHostProfile(rows) if err != nil { return nil, err } result[profile.UserID] = profile } return result, rows.Err() } // GetBDProfile 按 role 读取普通 BD 或 BD Leader 身份事实。 func (r *Repository) GetBDProfile(ctx context.Context, userID int64, role string) (hostdomain.BDProfile, error) { if role == hostdomain.BDRoleLeader { return queryBDLeaderProfile(ctx, r.db, "WHERE user_id = ?", userID) } return queryBDProfile(ctx, r.db, "WHERE user_id = ?", userID) } // GetCoinSellerProfile 读取币商身份事实。 func (r *Repository) GetCoinSellerProfile(ctx context.Context, userID int64) (hostdomain.CoinSellerProfile, error) { return queryCoinSellerProfile(ctx, r.db, "WHERE user_id = ?", userID) } // GetAgency 读取 Agency 当前事实。 func (r *Repository) GetAgency(ctx context.Context, agencyID int64) (hostdomain.Agency, error) { return queryAgency(ctx, r.db, "WHERE agency_id = ?", agencyID) } // ListActiveCoinSellersInMyRegion 返回当前用户所在区域的启用币商列表。 // 该查询从 users.region_id 出发过滤,避免把区域条件交给 gateway 或客户端传入。 func (r *Repository) ListActiveCoinSellersInMyRegion(ctx context.Context, userID int64) ([]hostdomain.CoinSellerListItem, error) { regionID, err := r.userRegion(ctx, r.db, userID, "") if err != nil { return nil, err } if regionID <= 0 { // 币商列表是区域内充值入口,缺少当前区域时不能降级成全局列表。 return nil, xerr.New(xerr.InvalidArgument, "user region is required") } if err := requireActiveRegion(ctx, r.db, regionID); err != nil { return nil, err } rows, err := r.db.QueryContext(ctx, ` SELECT u.user_id, u.current_display_user_id, COALESCE(u.username, ''), COALESCE(u.avatar, ''), COALESCE(country.country_id, 0), COALESCE(u.country, ''), COALESCE(country.country_name, ''), COALESCE(country.country_display_name, ''), u.region_id, COALESCE(region.region_code, ''), COALESCE(region.name, ''), seller.status, seller.merchant_asset_type, COALESCE(NULLIF(u.contact_info, ''), seller.contact_info, ''), seller.updated_at_ms FROM users u INNER JOIN coin_seller_profiles seller ON seller.app_code = u.app_code AND seller.user_id = u.user_id LEFT JOIN countries country ON country.app_code = u.app_code AND country.country_code = u.country LEFT JOIN regions region ON region.app_code = u.app_code AND region.region_id = u.region_id WHERE u.app_code = ? AND u.region_id = ? AND u.status = 'active' AND seller.status = ? AND seller.merchant_asset_type = ? ORDER BY seller.updated_at_ms DESC, u.user_id DESC `, appcode.FromContext(ctx), regionID, hostdomain.CoinSellerStatusActive, hostdomain.CoinSellerMerchantAssetType) if err != nil { return nil, err } defer rows.Close() items := make([]hostdomain.CoinSellerListItem, 0) for rows.Next() { var item hostdomain.CoinSellerListItem if err := rows.Scan( &item.UserID, &item.DisplayUserID, &item.Username, &item.Avatar, &item.CountryID, &item.CountryCode, &item.CountryName, &item.CountryDisplayName, &item.RegionID, &item.RegionCode, &item.RegionName, &item.Status, &item.MerchantAssetType, &item.ContactInfo, &item.UpdatedAtMs, ); err != nil { return nil, err } items = append(items, item) } return items, rows.Err() } // GetUserRoleSummary 用一组索引点查构造我的页角色摘要。 // 摘要只表达入口显隐需要的当前身份,不加载成员列表、团队统计或薪资数据。 func (r *Repository) GetUserRoleSummary(ctx context.Context, userID int64) (hostdomain.UserRoleSummary, error) { app := appcode.FromContext(ctx) summary := hostdomain.UserRoleSummary{UserID: userID} var hostStatus string var ownerAgencyID int64 var bdUserID int64 var bdStatus string var leaderUserID int64 var leaderStatus string var managerUserID int64 var coinSellerStatus string var merchantAssetType string err := r.db.QueryRowContext(ctx, ` SELECT COALESCE(h.status, ''), COALESCE(a.agency_id, 0), COALESCE(b.user_id, 0), COALESCE(b.status, ''), COALESCE(bl.user_id, 0), COALESCE(bl.status, ''), COALESCE(m.user_id, 0), COALESCE(c.status, ''), COALESCE(c.merchant_asset_type, '') FROM (SELECT ? AS user_id) u LEFT JOIN host_profiles h ON h.app_code = ? AND h.user_id = u.user_id LEFT JOIN agencies a ON a.app_code = ? AND a.owner_user_id = u.user_id AND a.status = ? LEFT JOIN bd_profiles b ON b.app_code = ? AND b.user_id = u.user_id LEFT JOIN bd_leader_profiles bl ON bl.app_code = ? AND bl.user_id = u.user_id LEFT JOIN manager_profiles m ON m.app_code = ? AND m.user_id = u.user_id AND m.status = ? LEFT JOIN coin_seller_profiles c ON c.app_code = ? AND c.user_id = u.user_id`, userID, app, app, hostdomain.AgencyStatusActive, app, app, app, hostdomain.ManagerStatusActive, app, ).Scan(&hostStatus, &ownerAgencyID, &bdUserID, &bdStatus, &leaderUserID, &leaderStatus, &managerUserID, &coinSellerStatus, &merchantAssetType) if err != nil { return hostdomain.UserRoleSummary{}, err } summary.HostStatus = hostStatus summary.IsHost = hostStatus == hostdomain.HostStatusActive // Agency 身份只来自 active Agency owner 行;普通 Host 即使有 current_agency_id,也只是成员归属,不能暴露 Agency Center。 summary.IsAgency = ownerAgencyID > 0 if summary.IsAgency { summary.AgencyID = ownerAgencyID } // 经理身份来自独立 manager_profiles 表;Agency owner、Host、BD 或 BD Leader 身份都不会隐式获得经理中心入口。 summary.IsManager = managerUserID > 0 summary.BDStatus = bdStatus if bdUserID > 0 && bdStatus == hostdomain.BDStatusActive { summary.BDID = bdUserID summary.IsBD = true } if leaderUserID > 0 && leaderStatus == hostdomain.BDStatusActive { if summary.BDID == 0 { summary.BDID = leaderUserID } if summary.BDStatus == "" { summary.BDStatus = leaderStatus } summary.IsBDLeader = true } summary.CoinSellerStatus = coinSellerStatus summary.IsCoinSeller = coinSellerStatus == hostdomain.CoinSellerStatusActive && merchantAssetType == hostdomain.CoinSellerMerchantAssetType if err := r.db.QueryRowContext(ctx, ` SELECT COUNT(*) FROM role_invitations WHERE app_code = ? AND target_user_id = ? AND status = ?`, app, userID, hostdomain.InvitationStatusPending, ).Scan(&summary.PendingRoleInvitations); err != nil { return hostdomain.UserRoleSummary{}, err } return summary, nil } // HasActiveAgencyOwner 精确判断用户是否为 active Agency owner。 // 普通 Agency 成员只保留 host_profile.current_agency_id 归属,不会在 UserRoleSummary 中获得 Agency 入口。 func (r *Repository) HasActiveAgencyOwner(ctx context.Context, userID int64) (int64, bool, error) { agency, ok, err := queryActiveAgencyByOwner(ctx, r.db, userID, false) if err != nil || !ok { return 0, ok, err } return agency.AgencyID, true, nil } // HasActiveManagerProfile 精确判断用户是否拥有 active 经理身份。 // 经理是 BD Leader/Admin 的上级身份,不能从 Agency owner 或 BD/BD Leader 身份推导出来。 func (r *Repository) HasActiveManagerProfile(ctx context.Context, userID int64) (bool, error) { var id int64 err := r.db.QueryRowContext(ctx, ` SELECT user_id FROM manager_profiles WHERE app_code = ? AND user_id = ? AND status = ? LIMIT 1 `, appcode.FromContext(ctx), userID, hostdomain.ManagerStatusActive).Scan(&id) if err == sql.ErrNoRows { return false, nil } if err != nil { return false, err } return id > 0, nil } // HasActiveManagerCapability 精确校验经理中心单项能力。 // 这里把 legacy 的 manager_resource_grant 收敛成任一资源赠送权限,写入口仍会按真实资源类型再校验一次。 func (r *Repository) HasActiveManagerCapability(ctx context.Context, userID int64, capability string) (bool, error) { columnSQL := "" switch capability { case hostservice.CapabilityManagerResourceGrant: columnSQL = "(can_grant_avatar_frame = 1 OR can_grant_vehicle = 1 OR can_grant_badge = 1)" case hostservice.CapabilityManagerGrantAvatarFrame: columnSQL = "can_grant_avatar_frame = 1" case hostservice.CapabilityManagerGrantVehicle: columnSQL = "can_grant_vehicle = 1" case hostservice.CapabilityManagerGrantBadge: columnSQL = "can_grant_badge = 1" case hostservice.CapabilityManagerGrantVIP: columnSQL = "can_grant_vip = 1" case hostservice.CapabilityManagerUpdateUserLevel: columnSQL = "can_update_user_level = 1" case hostservice.CapabilityManagerAddBDLeader: columnSQL = "can_add_bd_leader = 1" case hostservice.CapabilityManagerAddAdmin: columnSQL = "can_add_admin = 1" case hostservice.CapabilityManagerAddSuperadmin: columnSQL = "can_add_superadmin = 1" case hostservice.CapabilityManagerBlockUser: columnSQL = "can_block_user = 1" case hostservice.CapabilityManagerTransferCountry: columnSQL = "can_transfer_user_country = 1" default: return false, nil } var id int64 err := r.db.QueryRowContext(ctx, ` SELECT user_id FROM manager_profiles WHERE app_code = ? AND user_id = ? AND status = ? AND `+columnSQL+` LIMIT 1 `, appcode.FromContext(ctx), userID, hostdomain.ManagerStatusActive).Scan(&id) if err == sql.ErrNoRows { return false, nil } if err != nil { return false, err } return id > 0, nil } // ListBDLeaderBDs 读取 BD Leader 直属普通 BD;调用前先校验独立 Leader 行当前仍有效,避免停用后继续读取团队数据。 func (r *Repository) ListBDLeaderBDs(ctx context.Context, command hostservice.ListBDLeaderBDsCommand) ([]hostdomain.BDProfile, error) { leader, err := queryBDLeaderProfile(ctx, r.db, "WHERE user_id = ?", command.LeaderUserID) if err != nil { return nil, err } if leader.Status != hostdomain.BDStatusActive { return nil, xerr.New(xerr.PermissionDenied, "leader is not active") } status := command.Status if status == "" { status = hostdomain.BDStatusActive } clause, args := appScopedClause(ctx, "WHERE role = ? AND parent_leader_user_id = ? AND status = ? ORDER BY created_at_ms DESC, user_id DESC LIMIT ?", hostdomain.BDRoleBD, command.LeaderUserID, status, command.PageSize) return queryBDProfiles(ctx, r.db, clause, args...) } // ListBDLeaderAgencies 读取 BD Leader 团队 Agency;包含直属 leader 自己创建的 Agency 和下属普通 BD 创建的 Agency。 func (r *Repository) ListBDLeaderAgencies(ctx context.Context, command hostservice.ListBDLeaderAgenciesCommand) ([]hostdomain.Agency, error) { leader, err := queryBDLeaderProfile(ctx, r.db, "WHERE user_id = ?", command.LeaderUserID) if err != nil { return nil, err } if leader.Status != hostdomain.BDStatusActive { return nil, xerr.New(xerr.PermissionDenied, "leader is not active") } status := command.Status if status == "" { status = hostdomain.AgencyStatusActive } clause, args := appScopedClause(ctx, `WHERE status = ? AND ( parent_bd_user_id = ? OR parent_bd_user_id IN ( SELECT user_id FROM bd_profiles WHERE app_code = agencies.app_code AND role = ? AND parent_leader_user_id = ? AND status = ? ) ) ORDER BY created_at_ms DESC, agency_id DESC LIMIT ?`, status, command.LeaderUserID, hostdomain.BDRoleBD, command.LeaderUserID, hostdomain.BDStatusActive, command.PageSize) return queryAgencies(ctx, r.db, clause, args...) } // ListBDAgencies 只读取一个有效普通 BD 直接拓展的 Agency,不包含同负责人下其他 BD 的数据。 func (r *Repository) ListBDAgencies(ctx context.Context, command hostservice.ListBDAgenciesCommand) ([]hostdomain.Agency, error) { bd, err := queryBDProfile(ctx, r.db, "WHERE user_id = ?", command.BDUserID) if err != nil { return nil, err } if bd.Status != hostdomain.BDStatusActive || bd.Role != hostdomain.BDRoleBD { return nil, xerr.New(xerr.PermissionDenied, "bd is not active") } status := command.Status if status == "" { status = hostdomain.AgencyStatusActive } clause, args := appScopedClause(ctx, "WHERE status = ? AND parent_bd_user_id = ? ORDER BY created_at_ms DESC, agency_id DESC LIMIT ?", status, command.BDUserID, command.PageSize) return queryAgencies(ctx, r.db, clause, args...) } // ListRoleInvitations 读取目标用户收到的角色邀请消息;默认由业务层传入 pending 状态用于消息中心。 func (r *Repository) ListRoleInvitations(ctx context.Context, command hostservice.ListRoleInvitationsCommand) ([]hostdomain.RoleInvitation, error) { query := fmt.Sprintf(`SELECT %s FROM role_invitations WHERE app_code = ? AND target_user_id = ?`, roleInvitationColumns) args := []any{appcode.FromContext(ctx), command.TargetUserID} if command.Status != "" { query += " AND status = ?" args = append(args, command.Status) } // 消息中心优先展示最新邀请;同毫秒内用 invitation_id 保持稳定排序。 query += " ORDER BY created_at_ms DESC, invitation_id DESC LIMIT ?" args = append(args, command.PageSize) return queryRoleInvitations(ctx, r.db, query, args...) } // ListAgencyMembers 读取 Agency 成员关系列表。 func (r *Repository) ListAgencyMembers(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyMembership, error) { query := fmt.Sprintf(`SELECT %s FROM agency_memberships WHERE app_code = ? AND agency_id = ?`, agencyMembershipColumns) args := []any{appcode.FromContext(ctx), agencyID} if status != "" { query += " AND status = ?" args = append(args, status) } query += " ORDER BY joined_at_ms DESC, membership_id DESC" return queryAgencyMemberships(ctx, r.db, query, args...) } // ListAgencyApplications 读取 Agency 申请列表。 func (r *Repository) ListAgencyApplications(ctx context.Context, agencyID int64, status string) ([]hostdomain.AgencyApplication, error) { query := fmt.Sprintf(`SELECT %s FROM agency_applications WHERE app_code = ? AND agency_id = ?`, agencyApplicationColumns) args := []any{appcode.FromContext(ctx), agencyID} if status != "" { query += " AND status = ?" args = append(args, status) } query += " ORDER BY created_at_ms DESC, application_id DESC" return queryAgencyApplications(ctx, r.db, query, args...) } func queryHostProfile(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.HostProfile, error) { clause, args = appScopedClause(ctx, clause, args...) profile, err := scanHostProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM host_profiles %s`, hostProfileColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.HostProfile{}, xerr.New(xerr.NotFound, "host profile not found") } return profile, err } func queryHostProfileMaybe(ctx context.Context, q sqlQueryer, userID int64, lock bool) (hostdomain.HostProfile, bool, error) { clause := "WHERE user_id = ? AND status = ?" if lock { clause += " FOR UPDATE" } clause, args := appScopedClause(ctx, clause, userID, hostdomain.HostStatusActive) profile, err := scanHostProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM host_profiles %s`, hostProfileColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.HostProfile{}, false, nil } if err != nil { return hostdomain.HostProfile{}, false, err } return profile, true, nil } func queryHostProfileByUserMaybe(ctx context.Context, q sqlQueryer, userID int64, lock bool) (hostdomain.HostProfile, bool, error) { clause := "WHERE user_id = ?" if lock { // 后台创建 Agency owner 时需要覆盖 active/disabled 任意主播身份行,避免重复插入主键。 clause += " FOR UPDATE" } clause, args := appScopedClause(ctx, clause, userID) profile, err := scanHostProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM host_profiles %s`, hostProfileColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.HostProfile{}, false, nil } if err != nil { return hostdomain.HostProfile{}, false, err } return profile, true, nil } func scanHostProfile(scanner rowScanner) (hostdomain.HostProfile, error) { var profile hostdomain.HostProfile err := scanner.Scan( &profile.UserID, &profile.Status, &profile.RegionID, &profile.CurrentAgencyID, &profile.CurrentMembershipID, &profile.CurrentAgencyOwnerUserID, &profile.Source, &profile.FirstBecameHostAtMs, &profile.CreatedAtMs, &profile.UpdatedAtMs, ) return profile, err } func queryAgency(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.Agency, error) { clause, args = appScopedClause(ctx, clause, args...) agency, err := scanAgency(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM agencies %s`, agencyColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.Agency{}, xerr.New(xerr.NotFound, "agency not found") } return agency, err } func queryAgencies(ctx context.Context, q sqlQueryer, clause string, args ...any) ([]hostdomain.Agency, error) { rows, err := q.QueryContext(ctx, fmt.Sprintf(`SELECT %s FROM agencies %s`, agencyColumns, clause), args...) if err != nil { return nil, err } defer rows.Close() agencies := make([]hostdomain.Agency, 0) for rows.Next() { agency, err := scanAgency(rows) if err != nil { return nil, err } agencies = append(agencies, agency) } return agencies, rows.Err() } func queryActiveAgencyByOwner(ctx context.Context, q sqlQueryer, ownerUserID int64, lock bool) (hostdomain.Agency, bool, error) { clause := "WHERE owner_user_id = ? AND status = ?" if lock { clause += " FOR UPDATE" } clause, args := appScopedClause(ctx, clause, ownerUserID, hostdomain.AgencyStatusActive) agency, err := scanAgency(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM agencies %s`, agencyColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.Agency{}, false, nil } if err != nil { return hostdomain.Agency{}, false, err } return agency, true, nil } func queryAgencyByOwnerMaybe(ctx context.Context, q sqlQueryer, ownerUserID int64, lock bool) (hostdomain.Agency, bool, error) { clause := "WHERE owner_user_id = ?" if lock { // 后台创建 Agency 时要锁住 owner 名下所有历史 Agency 行,避免 active/closed 切换和新建并发。 clause += " FOR UPDATE" } clause, args := appScopedClause(ctx, clause, ownerUserID) agency, err := scanAgency(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM agencies %s`, agencyColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.Agency{}, false, nil } if err != nil { return hostdomain.Agency{}, false, err } return agency, true, nil } func scanAgency(scanner rowScanner) (hostdomain.Agency, error) { var agency hostdomain.Agency err := scanner.Scan( &agency.AgencyID, &agency.OwnerUserID, &agency.RegionID, &agency.ParentBDUserID, &agency.Name, &agency.Avatar, &agency.Status, &agency.JoinEnabled, &agency.MaxHosts, &agency.CreatedByUserID, &agency.CreatedAtMs, &agency.UpdatedAtMs, &agency.ActiveHostCount, ) return agency, err } func scanSearchAgency(scanner rowScanner) (hostdomain.Agency, error) { var agency hostdomain.Agency err := scanner.Scan( &agency.AgencyID, &agency.OwnerUserID, &agency.RegionID, &agency.ParentBDUserID, &agency.Name, &agency.Avatar, &agency.Status, &agency.JoinEnabled, &agency.MaxHosts, &agency.CreatedByUserID, &agency.CreatedAtMs, &agency.UpdatedAtMs, &agency.ActiveHostCount, ) return agency, err } func int64FromKeyword(keyword string) int64 { value, err := strconv.ParseInt(strings.TrimSpace(keyword), 10, 64) if err != nil { return 0 } return value } func queryBDProfile(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.BDProfile, error) { clause, args = appScopedClause(ctx, clause, args...) profile, err := scanBDProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM bd_profiles %s`, bdProfileColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.BDProfile{}, xerr.New(xerr.NotFound, "bd profile not found") } return profile, err } func queryBDProfiles(ctx context.Context, q sqlQueryer, clause string, args ...any) ([]hostdomain.BDProfile, error) { rows, err := q.QueryContext(ctx, fmt.Sprintf(`SELECT %s FROM bd_profiles %s`, bdProfileColumns, clause), args...) if err != nil { return nil, err } defer rows.Close() profiles := make([]hostdomain.BDProfile, 0) for rows.Next() { profile, err := scanBDProfile(rows) if err != nil { return nil, err } profiles = append(profiles, profile) } return profiles, rows.Err() } func queryBDLeaderProfile(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.BDProfile, error) { clause, args = appScopedClause(ctx, clause, args...) profile, err := scanBDProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM bd_leader_profiles %s`, bdLeaderProfileColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.BDProfile{}, xerr.New(xerr.NotFound, "bd leader profile not found") } return profile, err } func queryBDProfileMaybe(ctx context.Context, q sqlQueryer, userID int64, lock bool) (hostdomain.BDProfile, bool, error) { clause := "WHERE user_id = ? AND status = ?" if lock { clause += " FOR UPDATE" } clause, args := appScopedClause(ctx, clause, userID, hostdomain.BDStatusActive) profile, err := scanBDProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM bd_profiles %s`, bdProfileColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.BDProfile{}, false, nil } if err != nil { return hostdomain.BDProfile{}, false, err } return profile, true, nil } func queryBDLeaderProfileMaybe(ctx context.Context, q sqlQueryer, userID int64, lock bool) (hostdomain.BDProfile, bool, error) { clause := "WHERE user_id = ? AND status = ?" if lock { clause += " FOR UPDATE" } clause, args := appScopedClause(ctx, clause, userID, hostdomain.BDStatusActive) profile, err := scanBDProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM bd_leader_profiles %s`, bdLeaderProfileColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.BDProfile{}, false, nil } if err != nil { return hostdomain.BDProfile{}, false, err } return profile, true, nil } func queryBDProfileByUserMaybe(ctx context.Context, q sqlQueryer, userID int64, lock bool) (hostdomain.BDProfile, bool, error) { clause := "WHERE user_id = ?" if lock { // 后台创建/停用角色使用主键锁,覆盖 active 和 disabled 两种状态。 clause += " FOR UPDATE" } clause, args := appScopedClause(ctx, clause, userID) profile, err := scanBDProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM bd_profiles %s`, bdProfileColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.BDProfile{}, false, nil } if err != nil { return hostdomain.BDProfile{}, false, err } return profile, true, nil } func queryBDLeaderProfileByUserMaybe(ctx context.Context, q sqlQueryer, userID int64, lock bool) (hostdomain.BDProfile, bool, error) { clause := "WHERE user_id = ?" if lock { // 后台创建/停用 Leader 使用主键锁,覆盖 active 和 disabled 两种状态。 clause += " FOR UPDATE" } clause, args := appScopedClause(ctx, clause, userID) profile, err := scanBDProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM bd_leader_profiles %s`, bdLeaderProfileColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.BDProfile{}, false, nil } if err != nil { return hostdomain.BDProfile{}, false, err } return profile, true, nil } func scanBDProfile(scanner rowScanner) (hostdomain.BDProfile, error) { var profile hostdomain.BDProfile err := scanner.Scan( &profile.UserID, &profile.Role, &profile.RegionID, &profile.ParentLeaderUserID, &profile.Status, &profile.CreatedByUserID, &profile.CreatedAtMs, &profile.UpdatedAtMs, ) return profile, err } func queryCoinSellerProfile(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.CoinSellerProfile, error) { clause, args = appScopedClause(ctx, clause, args...) profile, err := scanCoinSellerProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM coin_seller_profiles %s`, coinSellerProfileColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.CoinSellerProfile{}, xerr.New(xerr.NotFound, "coin_seller profile not found") } return profile, err } func queryCoinSellerProfileByUserMaybe(ctx context.Context, q sqlQueryer, userID int64, lock bool) (hostdomain.CoinSellerProfile, bool, error) { clause := "WHERE user_id = ?" if lock { // 币商身份可以和 Agency/BD/BD Leader 并存,但同一用户只能有一条币商身份事实。 clause += " FOR UPDATE" } clause, args := appScopedClause(ctx, clause, userID) profile, err := scanCoinSellerProfile(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM coin_seller_profiles %s`, coinSellerProfileColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.CoinSellerProfile{}, false, nil } if err != nil { return hostdomain.CoinSellerProfile{}, false, err } return profile, true, nil } func scanCoinSellerProfile(scanner rowScanner) (hostdomain.CoinSellerProfile, error) { var profile hostdomain.CoinSellerProfile err := scanner.Scan( &profile.UserID, &profile.Status, &profile.MerchantAssetType, &profile.CreatedByUserID, &profile.CreatedAtMs, &profile.UpdatedAtMs, ) return profile, err } func queryAgencyMembership(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.AgencyMembership, error) { clause, args = appScopedClause(ctx, clause, args...) membership, err := scanAgencyMembership(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM agency_memberships %s`, agencyMembershipColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.AgencyMembership{}, xerr.New(xerr.NotFound, "agency membership not found") } return membership, err } func queryActiveMembershipByHost(ctx context.Context, q sqlQueryer, hostUserID int64, lock bool) (hostdomain.AgencyMembership, bool, error) { clause := "WHERE host_user_id = ? AND status = ?" if lock { clause += " FOR UPDATE" } clause, args := appScopedClause(ctx, clause, hostUserID, hostdomain.MembershipStatusActive) membership, err := scanAgencyMembership(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM agency_memberships %s`, agencyMembershipColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.AgencyMembership{}, false, nil } if err != nil { return hostdomain.AgencyMembership{}, false, err } return membership, true, nil } func queryAgencyMemberships(ctx context.Context, q sqlQueryer, query string, args ...any) ([]hostdomain.AgencyMembership, error) { rows, err := q.QueryContext(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() memberships := make([]hostdomain.AgencyMembership, 0) for rows.Next() { membership, err := scanAgencyMembership(rows) if err != nil { return nil, err } memberships = append(memberships, membership) } return memberships, rows.Err() } func scanAgencyMembership(scanner rowScanner) (hostdomain.AgencyMembership, error) { var membership hostdomain.AgencyMembership err := scanner.Scan( &membership.MembershipID, &membership.AgencyID, &membership.HostUserID, &membership.RegionID, &membership.MembershipType, &membership.Status, &membership.JoinedAtMs, &membership.EndedAtMs, &membership.EndedByUserID, &membership.EndedReason, &membership.CreatedAtMs, &membership.UpdatedAtMs, ) return membership, err } func queryAgencyApplication(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.AgencyApplication, error) { clause, args = appScopedClause(ctx, clause, args...) application, err := scanAgencyApplication(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM agency_applications %s`, agencyApplicationColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.AgencyApplication{}, xerr.New(xerr.NotFound, "agency application not found") } return application, err } func queryAgencyApplications(ctx context.Context, q sqlQueryer, query string, args ...any) ([]hostdomain.AgencyApplication, error) { rows, err := q.QueryContext(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() applications := make([]hostdomain.AgencyApplication, 0) for rows.Next() { application, err := scanAgencyApplication(rows) if err != nil { return nil, err } applications = append(applications, application) } return applications, rows.Err() } func scanAgencyApplication(scanner rowScanner) (hostdomain.AgencyApplication, error) { var application hostdomain.AgencyApplication err := scanner.Scan( &application.ApplicationID, &application.CommandID, &application.ApplicantUserID, &application.AgencyID, &application.RegionID, &application.Status, &application.ReviewedByUserID, &application.ReviewReason, &application.ReviewedAtMs, &application.CreatedAtMs, &application.UpdatedAtMs, ) return application, err } func queryRoleInvitation(ctx context.Context, q sqlQueryer, clause string, args ...any) (hostdomain.RoleInvitation, error) { clause, args = appScopedClause(ctx, clause, args...) invitation, err := scanRoleInvitation(q.QueryRowContext(ctx, fmt.Sprintf(`SELECT %s FROM role_invitations %s`, roleInvitationColumns, clause), args...)) if err == sql.ErrNoRows { return hostdomain.RoleInvitation{}, xerr.New(xerr.NotFound, "role invitation not found") } return invitation, err } func queryRoleInvitations(ctx context.Context, q sqlQueryer, query string, args ...any) ([]hostdomain.RoleInvitation, error) { rows, err := q.QueryContext(ctx, query, args...) if err != nil { return nil, err } defer rows.Close() invitations := make([]hostdomain.RoleInvitation, 0) for rows.Next() { invitation, err := scanRoleInvitation(rows) if err != nil { return nil, err } invitations = append(invitations, invitation) } return invitations, rows.Err() } func scanRoleInvitation(scanner rowScanner) (hostdomain.RoleInvitation, error) { var invitation hostdomain.RoleInvitation err := scanner.Scan( &invitation.InvitationID, &invitation.CommandID, &invitation.InvitationType, &invitation.Status, &invitation.InviterUserID, &invitation.InviterBDUserID, &invitation.TargetUserID, &invitation.RegionID, &invitation.AgencyName, &invitation.ParentBDUserID, &invitation.ParentLeaderUserID, &invitation.ProcessedByUserID, &invitation.ProcessReason, &invitation.ProcessedAtMs, &invitation.CreatedAtMs, &invitation.UpdatedAtMs, ) return invitation, err } func reviewResultByApplicationID(ctx context.Context, q sqlQueryer, applicationID int64) (hostdomain.ReviewAgencyApplicationResult, error) { application, err := queryAgencyApplication(ctx, q, "WHERE application_id = ?", applicationID) if err != nil { return hostdomain.ReviewAgencyApplicationResult{}, err } result := hostdomain.ReviewAgencyApplicationResult{Application: application} if application.Status == hostdomain.ApplicationStatusApproved { profile, err := queryHostProfile(ctx, q, "WHERE user_id = ?", application.ApplicantUserID) if err != nil { return hostdomain.ReviewAgencyApplicationResult{}, err } membership, err := queryAgencyMembership(ctx, q, "WHERE membership_id = ?", profile.CurrentMembershipID) if err != nil { return hostdomain.ReviewAgencyApplicationResult{}, err } result.HostProfile = profile result.Membership = membership } return result, nil } func kickResultByMembershipID(ctx context.Context, q sqlQueryer, membershipID int64) (hostdomain.KickAgencyHostResult, error) { membership, err := queryAgencyMembership(ctx, q, "WHERE membership_id = ?", membershipID) if err != nil { return hostdomain.KickAgencyHostResult{}, err } profile, err := queryHostProfile(ctx, q, "WHERE user_id = ?", membership.HostUserID) if err != nil { return hostdomain.KickAgencyHostResult{}, err } return hostdomain.KickAgencyHostResult{Membership: membership, HostProfile: profile}, nil } func processResultByInvitationID(ctx context.Context, q sqlQueryer, invitationID int64) (hostdomain.ProcessRoleInvitationResult, error) { invitation, err := queryRoleInvitation(ctx, q, "WHERE invitation_id = ?", invitationID) if err != nil { return hostdomain.ProcessRoleInvitationResult{}, err } result := hostdomain.ProcessRoleInvitationResult{Invitation: invitation} if invitation.Status != hostdomain.InvitationStatusAccepted { return result, nil } // 只有接受动作会创建后续身份事实;按邀请类型回填前端需要立即刷新的最新关系。 switch invitation.InvitationType { case hostdomain.InvitationTypeAgency: if profile, err := queryHostProfile(ctx, q, "WHERE user_id = ?", invitation.TargetUserID); err == nil { result.HostProfile = profile if profile.CurrentMembershipID > 0 { result.Membership, _ = queryAgencyMembership(ctx, q, "WHERE membership_id = ?", profile.CurrentMembershipID) } } if agency, ok, err := queryActiveAgencyByOwner(ctx, q, invitation.TargetUserID, false); err != nil { return hostdomain.ProcessRoleInvitationResult{}, err } else if ok { result.Agency = agency } case hostdomain.InvitationTypeBD: if bd, err := queryBDProfile(ctx, q, "WHERE user_id = ?", invitation.TargetUserID); err == nil { result.BDProfile = bd } case hostdomain.InvitationTypeHost: if profile, err := queryHostProfile(ctx, q, "WHERE user_id = ?", invitation.TargetUserID); err == nil { result.HostProfile = profile if profile.CurrentMembershipID > 0 { result.Membership, _ = queryAgencyMembership(ctx, q, "WHERE membership_id = ?", profile.CurrentMembershipID) } if profile.CurrentAgencyID > 0 { result.Agency, _ = queryAgency(ctx, q, "WHERE agency_id = ?", profile.CurrentAgencyID) } } } return result, nil }