321 lines
11 KiB
Go
321 lines
11 KiB
Go
package hostorg
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"hyapp-admin-server/internal/appctx"
|
|
"hyapp-admin-server/internal/integration/userclient"
|
|
)
|
|
|
|
type Reader struct {
|
|
db *sql.DB
|
|
walletDB *sql.DB
|
|
}
|
|
|
|
func NewReader(db *sql.DB, walletDB *sql.DB) *Reader {
|
|
return &Reader{db: db, walletDB: walletDB}
|
|
}
|
|
|
|
type CoinSellerListItem struct {
|
|
UserID int64 `json:"userId,string"`
|
|
Status string `json:"status"`
|
|
MerchantAssetType string `json:"merchantAssetType"`
|
|
MerchantBalance int64 `json:"merchantBalance"`
|
|
DisplayUserID string `json:"displayUserId"`
|
|
Username string `json:"username"`
|
|
Avatar string `json:"avatar"`
|
|
RegionID int64 `json:"regionId"`
|
|
RegionName string `json:"regionName"`
|
|
CreatedByUserID int64 `json:"createdByUserId"`
|
|
CreatedAtMs int64 `json:"createdAtMs"`
|
|
UpdatedAtMs int64 `json:"updatedAtMs"`
|
|
}
|
|
|
|
// ListBDProfiles 只读 user-service 的 BD 事实表,避免后台列表需求改动 user-service 发布物。
|
|
func (r *Reader) ListBDProfiles(ctx context.Context, query listQuery, role string) ([]*userclient.BDProfile, int64, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, 0, errUserDBNotConfigured()
|
|
}
|
|
appCode := appctx.FromContext(ctx)
|
|
whereSQL := "FROM bd_profiles bp LEFT JOIN users u ON u.app_code = bp.app_code AND u.user_id = bp.user_id WHERE bp.app_code = ? AND bp.role = ?"
|
|
args := []any{appCode, role}
|
|
if query.Status != "" {
|
|
whereSQL += " AND bp.status = ?"
|
|
args = append(args, query.Status)
|
|
}
|
|
if query.RegionID > 0 {
|
|
whereSQL += " AND bp.region_id = ?"
|
|
args = append(args, query.RegionID)
|
|
}
|
|
if query.ParentLeaderUserID > 0 {
|
|
whereSQL += " AND bp.parent_leader_user_id = ?"
|
|
args = append(args, query.ParentLeaderUserID)
|
|
}
|
|
if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
|
|
like := "%" + keyword + "%"
|
|
whereSQL += " AND (CAST(bp.user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.username LIKE ?)"
|
|
args = append(args, like, like, like)
|
|
}
|
|
|
|
total, err := countRows(ctx, r.db, whereSQL, args...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
|
|
SELECT bp.user_id, bp.role, bp.region_id, COALESCE(bp.parent_leader_user_id, 0),
|
|
bp.status, bp.created_by_user_id, bp.created_at_ms, bp.updated_at_ms
|
|
%s
|
|
ORDER BY bp.created_at_ms DESC, bp.user_id DESC
|
|
LIMIT ? OFFSET ?
|
|
`, whereSQL), append(args, query.PageSize, offset(query.Page, query.PageSize))...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := make([]*userclient.BDProfile, 0, query.PageSize)
|
|
for rows.Next() {
|
|
item := &userclient.BDProfile{}
|
|
if err := rows.Scan(&item.UserID, &item.Role, &item.RegionID, &item.ParentLeaderUserID, &item.Status, &item.CreatedByUserID, &item.CreatedAtMs, &item.UpdatedAtMs); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, total, rows.Err()
|
|
}
|
|
|
|
// ListAgencies 只读 user-service 的 Agency 事实表;后台关闭和入会开关仍走写命令。
|
|
func (r *Reader) ListAgencies(ctx context.Context, query listQuery) ([]*userclient.Agency, int64, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, 0, errUserDBNotConfigured()
|
|
}
|
|
appCode := appctx.FromContext(ctx)
|
|
whereSQL := "FROM agencies a LEFT JOIN users owner ON owner.app_code = a.app_code AND owner.user_id = a.owner_user_id WHERE a.app_code = ?"
|
|
args := []any{appCode}
|
|
if query.Status != "" {
|
|
whereSQL += " AND a.status = ?"
|
|
args = append(args, query.Status)
|
|
}
|
|
if query.RegionID > 0 {
|
|
whereSQL += " AND a.region_id = ?"
|
|
args = append(args, query.RegionID)
|
|
}
|
|
if query.ParentBDUserID > 0 {
|
|
whereSQL += " AND a.parent_bd_user_id = ?"
|
|
args = append(args, query.ParentBDUserID)
|
|
}
|
|
if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
|
|
like := "%" + keyword + "%"
|
|
whereSQL += " AND (a.name LIKE ? OR CAST(a.agency_id AS CHAR) LIKE ? OR CAST(a.owner_user_id AS CHAR) LIKE ? OR owner.current_display_user_id LIKE ? OR owner.username LIKE ?)"
|
|
args = append(args, like, like, like, like, like)
|
|
}
|
|
|
|
total, err := countRows(ctx, r.db, whereSQL, args...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
|
|
SELECT a.agency_id, a.owner_user_id, a.region_id, a.parent_bd_user_id,
|
|
a.name, a.status, a.join_enabled, a.max_hosts,
|
|
a.created_by_user_id, a.created_at_ms, a.updated_at_ms
|
|
%s
|
|
ORDER BY a.created_at_ms DESC, a.agency_id DESC
|
|
LIMIT ? OFFSET ?
|
|
`, whereSQL), append(args, query.PageSize, offset(query.Page, query.PageSize))...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := make([]*userclient.Agency, 0, query.PageSize)
|
|
for rows.Next() {
|
|
item := &userclient.Agency{}
|
|
if err := rows.Scan(&item.AgencyID, &item.OwnerUserID, &item.RegionID, &item.ParentBDUserID, &item.Name, &item.Status, &item.JoinEnabled, &item.MaxHosts, &item.CreatedByUserID, &item.CreatedAtMs, &item.UpdatedAtMs); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, total, rows.Err()
|
|
}
|
|
|
|
// ListHostProfiles 只读主播身份事实;成员变更和主播状态变更不在这个列表接口里发生。
|
|
func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*userclient.HostProfile, int64, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, 0, errUserDBNotConfigured()
|
|
}
|
|
appCode := appctx.FromContext(ctx)
|
|
whereSQL := "FROM host_profiles hp LEFT JOIN users u ON u.app_code = hp.app_code AND u.user_id = hp.user_id LEFT JOIN agencies a ON a.app_code = hp.app_code AND a.agency_id = hp.current_agency_id WHERE hp.app_code = ?"
|
|
args := []any{appCode}
|
|
if query.Status != "" {
|
|
whereSQL += " AND hp.status = ?"
|
|
args = append(args, query.Status)
|
|
}
|
|
if query.RegionID > 0 {
|
|
whereSQL += " AND hp.region_id = ?"
|
|
args = append(args, query.RegionID)
|
|
}
|
|
if query.AgencyID > 0 {
|
|
whereSQL += " AND hp.current_agency_id = ?"
|
|
args = append(args, query.AgencyID)
|
|
}
|
|
if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
|
|
like := "%" + keyword + "%"
|
|
whereSQL += " AND (CAST(hp.user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.username LIKE ? OR a.name LIKE ?)"
|
|
args = append(args, like, like, like, like)
|
|
}
|
|
|
|
total, err := countRows(ctx, r.db, whereSQL, args...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
|
|
SELECT hp.user_id, hp.status, hp.region_id, COALESCE(hp.current_agency_id, 0),
|
|
COALESCE(hp.current_membership_id, 0), hp.source,
|
|
hp.first_became_host_at_ms, hp.created_at_ms, hp.updated_at_ms
|
|
%s
|
|
ORDER BY hp.created_at_ms DESC, hp.user_id DESC
|
|
LIMIT ? OFFSET ?
|
|
`, whereSQL), append(args, query.PageSize, offset(query.Page, query.PageSize))...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := make([]*userclient.HostProfile, 0, query.PageSize)
|
|
for rows.Next() {
|
|
item := &userclient.HostProfile{}
|
|
if err := rows.Scan(&item.UserID, &item.Status, &item.RegionID, &item.CurrentAgencyID, &item.CurrentMembershipID, &item.Source, &item.FirstBecameHostAtMs, &item.CreatedAtMs, &item.UpdatedAtMs); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, total, rows.Err()
|
|
}
|
|
|
|
// ListCoinSellers 聚合 user-service 身份事实和 wallet-service 币商余额,只做后台展示读模型。
|
|
func (r *Reader) ListCoinSellers(ctx context.Context, query listQuery) ([]*CoinSellerListItem, int64, error) {
|
|
if r == nil || r.db == nil {
|
|
return nil, 0, errUserDBNotConfigured()
|
|
}
|
|
appCode := appctx.FromContext(ctx)
|
|
whereSQL := `
|
|
FROM coin_seller_profiles csp
|
|
LEFT JOIN users u ON u.app_code = csp.app_code AND u.user_id = csp.user_id
|
|
LEFT JOIN regions r ON r.app_code = csp.app_code AND r.region_id = u.region_id
|
|
WHERE csp.app_code = ?`
|
|
args := []any{appCode}
|
|
if query.Status != "" {
|
|
whereSQL += " AND csp.status = ?"
|
|
args = append(args, query.Status)
|
|
}
|
|
if query.RegionID > 0 {
|
|
whereSQL += " AND u.region_id = ?"
|
|
args = append(args, query.RegionID)
|
|
}
|
|
if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
|
|
like := "%" + keyword + "%"
|
|
whereSQL += " AND (CAST(csp.user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.username LIKE ?)"
|
|
args = append(args, like, like, like)
|
|
}
|
|
|
|
total, err := countRows(ctx, r.db, whereSQL, args...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
|
|
SELECT csp.user_id, csp.status, csp.merchant_asset_type,
|
|
csp.created_by_user_id, csp.created_at_ms, csp.updated_at_ms,
|
|
COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''),
|
|
COALESCE(u.avatar, ''), COALESCE(u.region_id, 0),
|
|
COALESCE(r.name, '')
|
|
%s
|
|
ORDER BY csp.created_at_ms DESC, csp.user_id DESC
|
|
LIMIT ? OFFSET ?
|
|
`, whereSQL), append(args, query.PageSize, offset(query.Page, query.PageSize))...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := make([]*CoinSellerListItem, 0, query.PageSize)
|
|
userIDs := make([]int64, 0, query.PageSize)
|
|
for rows.Next() {
|
|
item := &CoinSellerListItem{}
|
|
if err := rows.Scan(
|
|
&item.UserID,
|
|
&item.Status,
|
|
&item.MerchantAssetType,
|
|
&item.CreatedByUserID,
|
|
&item.CreatedAtMs,
|
|
&item.UpdatedAtMs,
|
|
&item.DisplayUserID,
|
|
&item.Username,
|
|
&item.Avatar,
|
|
&item.RegionID,
|
|
&item.RegionName,
|
|
); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
items = append(items, item)
|
|
userIDs = append(userIDs, item.UserID)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
balances, err := r.coinSellerBalances(ctx, appCode, userIDs)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
for _, item := range items {
|
|
item.MerchantBalance = balances[item.UserID]
|
|
}
|
|
return items, total, nil
|
|
}
|
|
|
|
func (r *Reader) coinSellerBalances(ctx context.Context, appCode string, userIDs []int64) (map[int64]int64, error) {
|
|
result := make(map[int64]int64, len(userIDs))
|
|
if r == nil || r.walletDB == nil || len(userIDs) == 0 {
|
|
return result, nil
|
|
}
|
|
placeholders := strings.TrimRight(strings.Repeat("?,", len(userIDs)), ",")
|
|
args := []any{appCode, "COIN_SELLER_COIN"}
|
|
for _, userID := range userIDs {
|
|
args = append(args, userID)
|
|
}
|
|
rows, err := r.walletDB.QueryContext(ctx, fmt.Sprintf(`
|
|
SELECT user_id, available_amount
|
|
FROM wallet_accounts
|
|
WHERE app_code = ? AND asset_type = ? AND user_id IN (%s)
|
|
`, placeholders), args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
for rows.Next() {
|
|
var userID int64
|
|
var amount int64
|
|
if err := rows.Scan(&userID, &amount); err != nil {
|
|
return nil, err
|
|
}
|
|
result[userID] = amount
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
func countRows(ctx context.Context, db *sql.DB, whereSQL string, args ...any) (int64, error) {
|
|
var total int64
|
|
err := db.QueryRowContext(ctx, "SELECT COUNT(*) "+whereSQL, args...).Scan(&total)
|
|
return total, err
|
|
}
|
|
|
|
func offset(page int, pageSize int) int {
|
|
return (page - 1) * pageSize
|
|
}
|
|
|
|
func errUserDBNotConfigured() error {
|
|
return fmt.Errorf("user mysql is not configured")
|
|
}
|