2026-07-05 01:55:03 +08:00

1047 lines
37 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

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

package 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
}