权限相关
This commit is contained in:
parent
7744232add
commit
152bdf232e
@ -122,6 +122,13 @@ func (r *Reader) ListBDProfiles(ctx context.Context, query listQuery, role strin
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
limitSQL := "LIMIT ? OFFSET ?"
|
||||
queryArgs := append(args, query.PageSize, offset(query.Page, query.PageSize))
|
||||
if query.SortBy == sortBySalaryWallet {
|
||||
// 工资钱包余额来自 wallet-service 读模型,不能只排序 SQL 当前页;先取出所有匹配身份,补齐余额后再分页。
|
||||
limitSQL = ""
|
||||
queryArgs = args
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
|
||||
SELECT bp.user_id, bp.role, `+userCountryRegionIDSQL("user_region")+`, COALESCE(bp.parent_leader_user_id, 0),
|
||||
bp.status, bp.created_by_user_id, bp.created_at_ms, bp.updated_at_ms,
|
||||
@ -140,8 +147,8 @@ func (r *Reader) ListBDProfiles(ctx context.Context, query listQuery, role strin
|
||||
)
|
||||
%s
|
||||
ORDER BY bp.created_at_ms DESC, bp.user_id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`, whereSQL), append(args, query.PageSize, offset(query.Page, query.PageSize))...)
|
||||
%s
|
||||
`, whereSQL, limitSQL), queryArgs...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@ -189,6 +196,10 @@ func (r *Reader) ListBDProfiles(ctx context.Context, query listQuery, role strin
|
||||
if err := r.fillBDProfileSalaryWallets(ctx, role, items); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if query.SortBy == sortBySalaryWallet {
|
||||
sortBDProfilesBySalaryWallet(items, query.SortDirection)
|
||||
items = paginateBDProfiles(items, query.Page, query.PageSize)
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
@ -225,6 +236,13 @@ func (r *Reader) listBDLeaderProfiles(ctx context.Context, query listQuery) ([]*
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
limitSQL := "LIMIT ? OFFSET ?"
|
||||
queryArgs := append(args, query.PageSize, offset(query.Page, query.PageSize))
|
||||
if query.SortBy == sortBySalaryWallet {
|
||||
// BD Leader 工资余额同样在 wallet-service;排序必须等 Admin 工资钱包资产填充完成后再裁剪当前页。
|
||||
limitSQL = ""
|
||||
queryArgs = args
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
|
||||
SELECT bp.user_id, 'bd_leader', `+userCountryRegionIDSQL("user_region")+`, 0,
|
||||
bp.status, bp.created_by_user_id, bp.created_at_ms, bp.updated_at_ms,
|
||||
@ -242,8 +260,8 @@ func (r *Reader) listBDLeaderProfiles(ctx context.Context, query listQuery) ([]*
|
||||
)
|
||||
%s
|
||||
ORDER BY bp.created_at_ms DESC, bp.user_id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`, whereSQL), append(args, query.PageSize, offset(query.Page, query.PageSize))...)
|
||||
%s
|
||||
`, whereSQL, limitSQL), queryArgs...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@ -289,6 +307,10 @@ func (r *Reader) listBDLeaderProfiles(ctx context.Context, query listQuery) ([]*
|
||||
if err := r.fillBDProfileSalaryWallets(ctx, salaryWalletRoleBDLeader, items); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if query.SortBy == sortBySalaryWallet {
|
||||
sortBDProfilesBySalaryWallet(items, query.SortDirection)
|
||||
items = paginateBDProfiles(items, query.Page, query.PageSize)
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
@ -579,6 +601,13 @@ func (r *Reader) ListAgencies(ctx context.Context, query listQuery) ([]*userclie
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
limitSQL := "LIMIT ? OFFSET ?"
|
||||
queryArgs := append(args, query.PageSize, offset(query.Page, query.PageSize))
|
||||
if query.SortBy == sortBySalaryWallet {
|
||||
// Agency 展示的是 owner 的 Agency 工资钱包;先补余额再排序,保证跨页顺序和真实余额一致。
|
||||
limitSQL = ""
|
||||
queryArgs = args
|
||||
}
|
||||
rows, err := r.db.QueryContext(ctx, fmt.Sprintf(`
|
||||
SELECT a.agency_id, a.owner_user_id, `+userCountryRegionIDSQL("owner_region")+`, a.parent_bd_user_id,
|
||||
a.name, a.status, a.join_enabled, a.max_hosts,
|
||||
@ -589,8 +618,8 @@ func (r *Reader) ListAgencies(ctx context.Context, query listQuery) ([]*userclie
|
||||
COALESCE(parent_bd.avatar, ''), `+userCountryRegionNameSQL("owner_region")+`
|
||||
%s
|
||||
ORDER BY a.created_at_ms DESC, a.agency_id DESC
|
||||
LIMIT ? OFFSET ?
|
||||
`, whereSQL), append(args, query.PageSize, offset(query.Page, query.PageSize))...)
|
||||
%s
|
||||
`, whereSQL, limitSQL), queryArgs...)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
@ -629,6 +658,10 @@ func (r *Reader) ListAgencies(ctx context.Context, query listQuery) ([]*userclie
|
||||
if err := r.fillAgencySalaryWallets(ctx, items); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if query.SortBy == sortBySalaryWallet {
|
||||
sortAgenciesBySalaryWallet(items, query.SortDirection)
|
||||
items = paginateAgencies(items, query.Page, query.PageSize)
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
@ -676,7 +709,8 @@ func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*user
|
||||
}
|
||||
limitSQL := "LIMIT ? OFFSET ?"
|
||||
queryArgs := append(args, query.PageSize, offset(query.Page, query.PageSize))
|
||||
if query.SortBy == "diamond" {
|
||||
if query.SortBy == sortByDiamond || query.SortBy == sortBySalaryWallet {
|
||||
// 主播钻石和工资余额都是列表外部聚合值;排序时取全量匹配集,聚合后稳定排序再分页。
|
||||
limitSQL = ""
|
||||
queryArgs = args
|
||||
}
|
||||
@ -739,10 +773,14 @@ func (r *Reader) ListHostProfiles(ctx context.Context, query listQuery) ([]*user
|
||||
if err := r.fillHostSalaryWallets(ctx, items); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if query.SortBy == "diamond" {
|
||||
if query.SortBy == sortByDiamond {
|
||||
sortHostProfilesByDiamond(items, query.SortDirection)
|
||||
items = paginateHostProfiles(items, query.Page, query.PageSize)
|
||||
}
|
||||
if query.SortBy == sortBySalaryWallet {
|
||||
sortHostProfilesBySalaryWallet(items, query.SortDirection)
|
||||
items = paginateHostProfiles(items, query.Page, query.PageSize)
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
@ -1131,6 +1169,50 @@ func coinSellerSortValue(item *CoinSellerListItem, sortBy string) int64 {
|
||||
}
|
||||
}
|
||||
|
||||
// sortBDProfilesBySalaryWallet 按当前身份的工资钱包可用余额排序;余额相同时保留 SQL 默认创建时间顺序,避免同值行跳动。
|
||||
func sortBDProfilesBySalaryWallet(items []*userclient.BDProfile, direction string) {
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
left := bdProfileSalaryWalletValue(items[i])
|
||||
right := bdProfileSalaryWalletValue(items[j])
|
||||
if left == right {
|
||||
return false
|
||||
}
|
||||
if direction == "asc" {
|
||||
return left < right
|
||||
}
|
||||
return left > right
|
||||
})
|
||||
}
|
||||
|
||||
func bdProfileSalaryWalletValue(item *userclient.BDProfile) int64 {
|
||||
if item == nil {
|
||||
return 0
|
||||
}
|
||||
return item.SalaryWallet.AvailableAmount
|
||||
}
|
||||
|
||||
// sortAgenciesBySalaryWallet 按 Agency owner 对应的 Agency 工资钱包余额排序,不混用 owner 的其他身份钱包。
|
||||
func sortAgenciesBySalaryWallet(items []*userclient.Agency, direction string) {
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
left := agencySalaryWalletValue(items[i])
|
||||
right := agencySalaryWalletValue(items[j])
|
||||
if left == right {
|
||||
return false
|
||||
}
|
||||
if direction == "asc" {
|
||||
return left < right
|
||||
}
|
||||
return left > right
|
||||
})
|
||||
}
|
||||
|
||||
func agencySalaryWalletValue(item *userclient.Agency) int64 {
|
||||
if item == nil {
|
||||
return 0
|
||||
}
|
||||
return item.SalaryWallet.AvailableAmount
|
||||
}
|
||||
|
||||
func sortHostProfilesByDiamond(items []*userclient.HostProfile, direction string) {
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
if items[i].Diamond == items[j].Diamond {
|
||||
@ -1143,6 +1225,28 @@ func sortHostProfilesByDiamond(items []*userclient.HostProfile, direction string
|
||||
})
|
||||
}
|
||||
|
||||
// sortHostProfilesBySalaryWallet 按主播工资钱包可用余额排序,和钻石排序一样在分页前处理跨页顺序。
|
||||
func sortHostProfilesBySalaryWallet(items []*userclient.HostProfile, direction string) {
|
||||
sort.SliceStable(items, func(i, j int) bool {
|
||||
left := hostProfileSalaryWalletValue(items[i])
|
||||
right := hostProfileSalaryWalletValue(items[j])
|
||||
if left == right {
|
||||
return false
|
||||
}
|
||||
if direction == "asc" {
|
||||
return left < right
|
||||
}
|
||||
return left > right
|
||||
})
|
||||
}
|
||||
|
||||
func hostProfileSalaryWalletValue(item *userclient.HostProfile) int64 {
|
||||
if item == nil {
|
||||
return 0
|
||||
}
|
||||
return item.SalaryWallet.AvailableAmount
|
||||
}
|
||||
|
||||
func paginateCoinSellerListItems(items []*CoinSellerListItem, page int, pageSize int) []*CoinSellerListItem {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
@ -1161,6 +1265,42 @@ func paginateCoinSellerListItems(items []*CoinSellerListItem, page int, pageSize
|
||||
return items[start:end]
|
||||
}
|
||||
|
||||
func paginateBDProfiles(items []*userclient.BDProfile, page int, pageSize int) []*userclient.BDProfile {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
start := (page - 1) * pageSize
|
||||
if start >= len(items) {
|
||||
return []*userclient.BDProfile{}
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
return items[start:end]
|
||||
}
|
||||
|
||||
func paginateAgencies(items []*userclient.Agency, page int, pageSize int) []*userclient.Agency {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 {
|
||||
pageSize = 20
|
||||
}
|
||||
start := (page - 1) * pageSize
|
||||
if start >= len(items) {
|
||||
return []*userclient.Agency{}
|
||||
}
|
||||
end := start + pageSize
|
||||
if end > len(items) {
|
||||
end = len(items)
|
||||
}
|
||||
return items[start:end]
|
||||
}
|
||||
|
||||
func paginateHostProfiles(items []*userclient.HostProfile, page int, pageSize int) []*userclient.HostProfile {
|
||||
if page < 1 {
|
||||
page = 1
|
||||
|
||||
@ -20,6 +20,7 @@ const (
|
||||
paidCurrencyUSDT = "USDT"
|
||||
sortByDiamond = "diamond"
|
||||
sortByMerchantBalance = "merchant_balance"
|
||||
sortBySalaryWallet = "salary_wallet"
|
||||
sortByTotalRechargeUSDT = "total_recharge_usdt"
|
||||
bdLeaderPositionAliasMaxRunes = 64
|
||||
)
|
||||
@ -589,7 +590,7 @@ func normalizeListQuery(query listQuery) listQuery {
|
||||
query.Status = strings.ToLower(strings.TrimSpace(query.Status))
|
||||
query.SortBy = strings.ToLower(strings.TrimSpace(query.SortBy))
|
||||
switch query.SortBy {
|
||||
case sortByDiamond, sortByMerchantBalance, sortByTotalRechargeUSDT:
|
||||
case sortByDiamond, sortByMerchantBalance, sortBySalaryWallet, sortByTotalRechargeUSDT:
|
||||
default:
|
||||
query.SortBy = ""
|
||||
query.SortDirection = ""
|
||||
|
||||
@ -3,11 +3,13 @@ package hostorg
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"hyapp-admin-server/internal/integration/userclient"
|
||||
)
|
||||
|
||||
// TestNormalizeListQueryAllowsCoinSellerComputedSorts 锁定币商余额和累充 USDT 排序字段会被后台列表查询接受。
|
||||
func TestNormalizeListQueryAllowsCoinSellerComputedSorts(t *testing.T) {
|
||||
for _, sortBy := range []string{sortByMerchantBalance, sortByTotalRechargeUSDT} {
|
||||
// TestNormalizeListQueryAllowsComputedSorts 锁定后台列表支持的计算排序字段不会在查询归一化时被清掉。
|
||||
func TestNormalizeListQueryAllowsComputedSorts(t *testing.T) {
|
||||
for _, sortBy := range []string{sortByMerchantBalance, sortBySalaryWallet, sortByTotalRechargeUSDT} {
|
||||
query := normalizeListQuery(listQuery{Page: 1, PageSize: 50, SortBy: sortBy, SortDirection: "asc"})
|
||||
if query.SortBy != sortBy || query.SortDirection != "asc" {
|
||||
t.Fatalf("expected sort %s asc to be kept, got sort_by=%q direction=%q", sortBy, query.SortBy, query.SortDirection)
|
||||
@ -96,3 +98,45 @@ func TestSortCoinSellerListItemsComputedFields(t *testing.T) {
|
||||
t.Fatalf("total recharge usdt asc order mismatch: %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSortSalaryWalletComputedFields 覆盖四个组织列表的工资钱包余额排序;这些余额来自 wallet-service,必须在分页前排序。
|
||||
func TestSortSalaryWalletComputedFields(t *testing.T) {
|
||||
bds := []*userclient.BDProfile{
|
||||
{UserID: 1, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 100}},
|
||||
{UserID: 2, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 300}},
|
||||
{UserID: 3, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 200}},
|
||||
}
|
||||
sortBDProfilesBySalaryWallet(bds, "desc")
|
||||
if got := []int64{bds[0].UserID, bds[1].UserID, bds[2].UserID}; got[0] != 2 || got[1] != 3 || got[2] != 1 {
|
||||
t.Fatalf("bd salary wallet desc order mismatch: %v", got)
|
||||
}
|
||||
if page := paginateBDProfiles(bds, 2, 2); len(page) != 1 || page[0].UserID != 1 {
|
||||
t.Fatalf("bd salary wallet pagination mismatch: %+v", page)
|
||||
}
|
||||
|
||||
agencies := []*userclient.Agency{
|
||||
{AgencyID: 10, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 100}},
|
||||
{AgencyID: 20, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 300}},
|
||||
{AgencyID: 30, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 200}},
|
||||
}
|
||||
sortAgenciesBySalaryWallet(agencies, "asc")
|
||||
if got := []int64{agencies[0].AgencyID, agencies[1].AgencyID, agencies[2].AgencyID}; got[0] != 10 || got[1] != 30 || got[2] != 20 {
|
||||
t.Fatalf("agency salary wallet asc order mismatch: %v", got)
|
||||
}
|
||||
if page := paginateAgencies(agencies, 2, 2); len(page) != 1 || page[0].AgencyID != 20 {
|
||||
t.Fatalf("agency salary wallet pagination mismatch: %+v", page)
|
||||
}
|
||||
|
||||
hosts := []*userclient.HostProfile{
|
||||
{UserID: 1, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 100}},
|
||||
{UserID: 2, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 300}},
|
||||
{UserID: 3, SalaryWallet: userclient.SalaryWallet{AvailableAmount: 200}},
|
||||
}
|
||||
sortHostProfilesBySalaryWallet(hosts, "desc")
|
||||
if got := []int64{hosts[0].UserID, hosts[1].UserID, hosts[2].UserID}; got[0] != 2 || got[1] != 3 || got[2] != 1 {
|
||||
t.Fatalf("host salary wallet desc order mismatch: %v", got)
|
||||
}
|
||||
if page := paginateHostProfiles(hosts, 2, 2); len(page) != 1 || page[0].UserID != 1 {
|
||||
t.Fatalf("host salary wallet pagination mismatch: %+v", page)
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
|
||||
|
||||
-- 线上生产环境关闭 bootstrap 和 AutoMigrate,新按钮权限必须通过 migration 落库,
|
||||
-- 否则前端即使已经上线,也会因为登录权限列表缺少该 code 而隐藏“工资兑换比例”按钮。
|
||||
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES
|
||||
('币商工资兑换比例', 'coin-seller:exchange-rate', 'button', '', @now_ms, @now_ms)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
name = VALUES(name),
|
||||
kind = VALUES(kind),
|
||||
description = VALUES(description),
|
||||
updated_at_ms = @now_ms;
|
||||
|
||||
-- 平台管理员应拥有全部后台能力;运维管理员也需要维护币商工资转金币比例。
|
||||
-- INSERT IGNORE 保持幂等,重复执行不会产生重复绑定。
|
||||
INSERT IGNORE INTO admin_role_permissions (role_id, permission_id)
|
||||
SELECT admin_role.id, admin_permission.id
|
||||
FROM admin_roles admin_role
|
||||
JOIN admin_permissions admin_permission
|
||||
WHERE admin_role.code IN ('platform-admin', 'ops-admin')
|
||||
AND admin_permission.code = 'coin-seller:exchange-rate';
|
||||
Loading…
x
Reference in New Issue
Block a user