拆分 BD 与 BD Leader 身份模型
This commit is contained in:
parent
f172ed8178
commit
0dea7c9b43
@ -2513,6 +2513,7 @@ type GetBDProfileRequest struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"`
|
||||
UserId int64 `protobuf:"varint,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||
Role string `protobuf:"bytes,3,opt,name=role,proto3" json:"role,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@ -2561,6 +2562,13 @@ func (x *GetBDProfileRequest) GetUserId() int64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *GetBDProfileRequest) GetRole() string {
|
||||
if x != nil {
|
||||
return x.Role
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type GetBDProfileResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
BdProfile *BDProfile `protobuf:"bytes,1,opt,name=bd_profile,json=bdProfile,proto3" json:"bd_profile,omitempty"`
|
||||
@ -3565,6 +3573,7 @@ type SetBDStatusRequest struct {
|
||||
TargetUserId int64 `protobuf:"varint,4,opt,name=target_user_id,json=targetUserId,proto3" json:"target_user_id,omitempty"`
|
||||
Status string `protobuf:"bytes,5,opt,name=status,proto3" json:"status,omitempty"`
|
||||
Reason string `protobuf:"bytes,6,opt,name=reason,proto3" json:"reason,omitempty"`
|
||||
Role string `protobuf:"bytes,7,opt,name=role,proto3" json:"role,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@ -3641,6 +3650,13 @@ func (x *SetBDStatusRequest) GetReason() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SetBDStatusRequest) GetRole() string {
|
||||
if x != nil {
|
||||
return x.Role
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type SetBDStatusResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
BdProfile *BDProfile `protobuf:"bytes,1,opt,name=bd_profile,json=bdProfile,proto3" json:"bd_profile,omitempty"`
|
||||
@ -4717,10 +4733,11 @@ const file_proto_user_v1_host_proto_rawDesc = "" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x17\n" +
|
||||
"\auser_id\x18\x02 \x01(\x03R\x06userId\"W\n" +
|
||||
"\x16GetHostProfileResponse\x12=\n" +
|
||||
"\fhost_profile\x18\x01 \x01(\v2\x1a.hyapp.user.v1.HostProfileR\vhostProfile\"^\n" +
|
||||
"\fhost_profile\x18\x01 \x01(\v2\x1a.hyapp.user.v1.HostProfileR\vhostProfile\"r\n" +
|
||||
"\x13GetBDProfileRequest\x12.\n" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x17\n" +
|
||||
"\auser_id\x18\x02 \x01(\x03R\x06userId\"O\n" +
|
||||
"\auser_id\x18\x02 \x01(\x03R\x06userId\x12\x12\n" +
|
||||
"\x04role\x18\x03 \x01(\tR\x04role\"O\n" +
|
||||
"\x14GetBDProfileResponse\x127\n" +
|
||||
"\n" +
|
||||
"bd_profile\x18\x01 \x01(\v2\x18.hyapp.user.v1.BDProfileR\tbdProfile\"f\n" +
|
||||
@ -4785,7 +4802,7 @@ const file_proto_user_v1_host_proto_rawDesc = "" +
|
||||
"\x06reason\x18\x06 \x01(\tR\x06reason\"K\n" +
|
||||
"\x10CreateBDResponse\x127\n" +
|
||||
"\n" +
|
||||
"bd_profile\x18\x01 \x01(\v2\x18.hyapp.user.v1.BDProfileR\tbdProfile\"\xdd\x01\n" +
|
||||
"bd_profile\x18\x01 \x01(\v2\x18.hyapp.user.v1.BDProfileR\tbdProfile\"\xf1\x01\n" +
|
||||
"\x12SetBDStatusRequest\x12.\n" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.user.v1.RequestMetaR\x04meta\x12\x1d\n" +
|
||||
"\n" +
|
||||
@ -4793,7 +4810,8 @@ const file_proto_user_v1_host_proto_rawDesc = "" +
|
||||
"\radmin_user_id\x18\x03 \x01(\x03R\vadminUserId\x12$\n" +
|
||||
"\x0etarget_user_id\x18\x04 \x01(\x03R\ftargetUserId\x12\x16\n" +
|
||||
"\x06status\x18\x05 \x01(\tR\x06status\x12\x16\n" +
|
||||
"\x06reason\x18\x06 \x01(\tR\x06reason\"N\n" +
|
||||
"\x06reason\x18\x06 \x01(\tR\x06reason\x12\x12\n" +
|
||||
"\x04role\x18\a \x01(\tR\x04role\"N\n" +
|
||||
"\x13SetBDStatusResponse\x127\n" +
|
||||
"\n" +
|
||||
"bd_profile\x18\x01 \x01(\v2\x18.hyapp.user.v1.BDProfileR\tbdProfile\"\xca\x01\n" +
|
||||
|
||||
@ -284,6 +284,7 @@ message GetHostProfileResponse {
|
||||
message GetBDProfileRequest {
|
||||
RequestMeta meta = 1;
|
||||
int64 user_id = 2;
|
||||
string role = 3;
|
||||
}
|
||||
|
||||
message GetBDProfileResponse {
|
||||
@ -389,6 +390,7 @@ message SetBDStatusRequest {
|
||||
int64 target_user_id = 4;
|
||||
string status = 5;
|
||||
string reason = 6;
|
||||
string role = 7;
|
||||
}
|
||||
|
||||
message SetBDStatusResponse {
|
||||
|
||||
26
scripts/mysql/038_split_bd_leader_profiles.sql
Normal file
26
scripts/mysql/038_split_bd_leader_profiles.sql
Normal file
@ -0,0 +1,26 @@
|
||||
CREATE TABLE IF NOT EXISTS bd_leader_profiles (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||
user_id BIGINT NOT NULL PRIMARY KEY COMMENT '用户 ID',
|
||||
region_id BIGINT NOT NULL COMMENT '区域 ID',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
created_by_user_id BIGINT NOT NULL COMMENT '创建人用户 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
KEY idx_bd_leader_profiles_region_status (app_code, region_id, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='BD Leader资料表';
|
||||
|
||||
INSERT INTO bd_leader_profiles (
|
||||
app_code, user_id, region_id, status, created_by_user_id, created_at_ms, updated_at_ms
|
||||
)
|
||||
SELECT app_code, user_id, region_id, status, created_by_user_id, created_at_ms, updated_at_ms
|
||||
FROM bd_profiles
|
||||
WHERE role = 'bd_leader'
|
||||
ON DUPLICATE KEY UPDATE
|
||||
region_id = VALUES(region_id),
|
||||
status = VALUES(status),
|
||||
created_by_user_id = VALUES(created_by_user_id),
|
||||
created_at_ms = VALUES(created_at_ms),
|
||||
updated_at_ms = VALUES(updated_at_ms);
|
||||
|
||||
DELETE FROM bd_profiles
|
||||
WHERE role = 'bd_leader';
|
||||
@ -317,6 +317,7 @@ type SetBDStatusRequest struct {
|
||||
CommandID string
|
||||
AdminUserID int64
|
||||
TargetUserID int64
|
||||
Role string
|
||||
Status string
|
||||
Reason string
|
||||
}
|
||||
@ -517,6 +518,7 @@ func (c *GRPCClient) SetBDStatus(ctx context.Context, req SetBDStatusRequest) (*
|
||||
CommandId: req.CommandID,
|
||||
AdminUserId: req.AdminUserID,
|
||||
TargetUserId: req.TargetUserID,
|
||||
Role: req.Role,
|
||||
Status: req.Status,
|
||||
Reason: req.Reason,
|
||||
})
|
||||
|
||||
@ -100,7 +100,7 @@ func (h *Handler) CreateBDLeader(c *gin.Context) {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
writeHostOrgAuditLog(c, h.audit, "create-bd-leader", "bd_profiles", profile.UserID,
|
||||
writeHostOrgAuditLog(c, h.audit, "create-bd-leader", "bd_leader_profiles", profile.UserID,
|
||||
fmt.Sprintf("command_id=%s user_id=%d region_id=%d position_alias=%q reason=%q", strings.TrimSpace(req.CommandID), profile.UserID, profile.RegionID, strings.TrimSpace(req.PositionAlias), strings.TrimSpace(req.Reason)))
|
||||
response.Created(c, profile)
|
||||
}
|
||||
@ -131,16 +131,24 @@ func (h *Handler) SetBDStatus(c *gin.Context) {
|
||||
response.BadRequest(c, "BD 状态参数不正确")
|
||||
return
|
||||
}
|
||||
profile, err := h.service.SetBDStatus(c.Request.Context(), adminActorID(c), targetUserID, middleware.CurrentRequestID(c), req)
|
||||
role, auditTarget := bdRoleFromStatusRoute(c)
|
||||
profile, err := h.service.SetBDStatus(c.Request.Context(), adminActorID(c), targetUserID, middleware.CurrentRequestID(c), role, req)
|
||||
if err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
writeHostOrgAuditLog(c, h.audit, "set-bd-status", "bd_profiles", profile.UserID,
|
||||
writeHostOrgAuditLog(c, h.audit, "set-bd-status", auditTarget, profile.UserID,
|
||||
fmt.Sprintf("command_id=%s user_id=%d status=%s reason=%q", strings.TrimSpace(req.CommandID), profile.UserID, profile.Status, strings.TrimSpace(req.Reason)))
|
||||
response.OK(c, profile)
|
||||
}
|
||||
|
||||
func bdRoleFromStatusRoute(c *gin.Context) (string, string) {
|
||||
if strings.HasPrefix(c.FullPath(), "/admin/bd-leaders/") {
|
||||
return "bd_leader", "bd_leader_profiles"
|
||||
}
|
||||
return "bd", "bd_profiles"
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateBDLeaderPositionAlias(c *gin.Context) {
|
||||
targetUserID, ok := parseInt64ID(c, "user_id")
|
||||
if !ok {
|
||||
@ -171,7 +179,7 @@ func (h *Handler) DeleteBDLeader(c *gin.Context) {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
writeHostOrgAuditLog(c, h.audit, "delete-bd-leader", "bd_profiles", profile.UserID,
|
||||
writeHostOrgAuditLog(c, h.audit, "delete-bd-leader", "bd_leader_profiles", profile.UserID,
|
||||
fmt.Sprintf("user_id=%d", profile.UserID))
|
||||
response.OK(c, profile)
|
||||
}
|
||||
|
||||
@ -53,11 +53,14 @@ type CoinSellerSalaryRateTier struct {
|
||||
UpdatedAtMs int64 `json:"updatedAtMs"`
|
||||
}
|
||||
|
||||
// ListBDProfiles 只读 user-service 的 BD 事实表,避免后台列表需求改动 user-service 发布物。
|
||||
// ListBDProfiles 只读 user-service 的 BD/BD Leader 事实表,后台列表按角色选择物理表,避免把两种身份重新混在一起。
|
||||
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()
|
||||
}
|
||||
if role == "bd_leader" {
|
||||
return r.listBDLeaderProfiles(ctx, query)
|
||||
}
|
||||
appCode := appctx.FromContext(ctx)
|
||||
whereSQL := `
|
||||
FROM bd_profiles bp
|
||||
@ -156,6 +159,101 @@ func (r *Reader) ListBDProfiles(ctx context.Context, query listQuery, role strin
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (r *Reader) listBDLeaderProfiles(ctx context.Context, query listQuery) ([]*userclient.BDProfile, int64, error) {
|
||||
appCode := appctx.FromContext(ctx)
|
||||
if query.ParentLeaderUserID > 0 {
|
||||
// Leader 身份没有上级 Leader 字段;带父级筛选时直接返回空结果,避免误用普通 BD 的父级语义。
|
||||
return []*userclient.BDProfile{}, 0, nil
|
||||
}
|
||||
whereSQL := `
|
||||
FROM bd_leader_profiles bp
|
||||
LEFT JOIN users u ON u.app_code = bp.app_code AND u.user_id = bp.user_id
|
||||
LEFT JOIN users creator ON creator.app_code = bp.app_code AND creator.user_id = bp.created_by_user_id
|
||||
LEFT JOIN regions r ON r.app_code = bp.app_code AND r.region_id = bp.region_id
|
||||
WHERE bp.app_code = ?`
|
||||
args := []any{appCode}
|
||||
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 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 ? OR r.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 bp.user_id, 'bd_leader', bp.region_id, 0,
|
||||
bp.status, bp.created_by_user_id, bp.created_at_ms, bp.updated_at_ms,
|
||||
COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''),
|
||||
COALESCE(u.avatar, ''), COALESCE(r.name, ''),
|
||||
'', '', '',
|
||||
COALESCE(creator.current_display_user_id, ''), COALESCE(creator.username, ''),
|
||||
COALESCE(creator.avatar, ''),
|
||||
(
|
||||
SELECT COUNT(1)
|
||||
FROM bd_profiles child
|
||||
WHERE child.app_code = bp.app_code
|
||||
AND child.role = 'bd'
|
||||
AND child.parent_leader_user_id = bp.user_id
|
||||
)
|
||||
%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,
|
||||
&item.DisplayUserID,
|
||||
&item.Username,
|
||||
&item.Avatar,
|
||||
&item.RegionName,
|
||||
&item.ParentLeaderDisplayID,
|
||||
&item.ParentLeaderUsername,
|
||||
&item.ParentLeaderAvatar,
|
||||
&item.CreatedByDisplayUserID,
|
||||
&item.CreatedByUsername,
|
||||
&item.CreatedByAvatar,
|
||||
&item.SubBDCount,
|
||||
); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := r.fillBDProfileCreators(ctx, items); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if err := r.fillBDLeaderPositionAliases(ctx, appCode, items); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return items, total, nil
|
||||
}
|
||||
|
||||
func (r *Reader) GetBDLeader(ctx context.Context, userID int64) (*userclient.BDProfile, error) {
|
||||
if r == nil || r.db == nil {
|
||||
return nil, errUserDBNotConfigured()
|
||||
@ -163,7 +261,7 @@ func (r *Reader) GetBDLeader(ctx context.Context, userID int64) (*userclient.BDP
|
||||
appCode := appctx.FromContext(ctx)
|
||||
item := &userclient.BDProfile{}
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT bp.user_id, bp.role, bp.region_id, COALESCE(bp.parent_leader_user_id, 0),
|
||||
SELECT bp.user_id, 'bd_leader', bp.region_id, 0,
|
||||
bp.status, bp.created_by_user_id, bp.created_at_ms, bp.updated_at_ms,
|
||||
COALESCE(u.current_display_user_id, ''), COALESCE(u.username, ''),
|
||||
COALESCE(u.avatar, ''), COALESCE(r.name, ''),
|
||||
@ -176,11 +274,11 @@ func (r *Reader) GetBDLeader(ctx context.Context, userID int64) (*userclient.BDP
|
||||
AND child.role = 'bd'
|
||||
AND child.parent_leader_user_id = bp.user_id
|
||||
)
|
||||
FROM bd_profiles bp
|
||||
FROM bd_leader_profiles bp
|
||||
LEFT JOIN users u ON u.app_code = bp.app_code AND u.user_id = bp.user_id
|
||||
LEFT JOIN users creator ON creator.app_code = bp.app_code AND creator.user_id = bp.created_by_user_id
|
||||
LEFT JOIN regions r ON r.app_code = bp.app_code AND r.region_id = bp.region_id
|
||||
WHERE bp.app_code = ? AND bp.user_id = ? AND bp.role = 'bd_leader'
|
||||
WHERE bp.app_code = ? AND bp.user_id = ?
|
||||
`, appCode, userID).Scan(
|
||||
&item.UserID,
|
||||
&item.Role,
|
||||
@ -351,10 +449,10 @@ func (r *Reader) DeleteBDLeader(ctx context.Context, userID int64) (*userclient.
|
||||
appCode := appctx.FromContext(ctx)
|
||||
item := &userclient.BDProfile{}
|
||||
err := r.db.QueryRowContext(ctx, `
|
||||
SELECT bp.user_id, bp.role, bp.region_id, COALESCE(bp.parent_leader_user_id, 0),
|
||||
SELECT bp.user_id, 'bd_leader', bp.region_id, 0,
|
||||
bp.status, bp.created_by_user_id, bp.created_at_ms, bp.updated_at_ms
|
||||
FROM bd_profiles bp
|
||||
WHERE bp.app_code = ? AND bp.user_id = ? AND bp.role = 'bd_leader'
|
||||
FROM bd_leader_profiles bp
|
||||
WHERE bp.app_code = ? AND bp.user_id = ?
|
||||
`, appCode, userID).Scan(
|
||||
&item.UserID,
|
||||
&item.Role,
|
||||
@ -372,8 +470,8 @@ func (r *Reader) DeleteBDLeader(ctx context.Context, userID int64) (*userclient.
|
||||
return nil, err
|
||||
}
|
||||
result, err := r.db.ExecContext(ctx, `
|
||||
DELETE FROM bd_profiles
|
||||
WHERE app_code = ? AND user_id = ? AND role = 'bd_leader'
|
||||
DELETE FROM bd_leader_profiles
|
||||
WHERE app_code = ? AND user_id = ?
|
||||
`, appCode, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@ -193,13 +193,14 @@ func (s *Service) CreateBD(ctx context.Context, actorID int64, requestID string,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Service) SetBDStatus(ctx context.Context, actorID int64, targetUserID int64, requestID string, req bdStatusRequest) (*userclient.BDProfile, error) {
|
||||
func (s *Service) SetBDStatus(ctx context.Context, actorID int64, targetUserID int64, requestID string, role string, req bdStatusRequest) (*userclient.BDProfile, error) {
|
||||
return s.userClient.SetBDStatus(ctx, userclient.SetBDStatusRequest{
|
||||
RequestID: requestID,
|
||||
Caller: "hyapp-admin-server",
|
||||
CommandID: strings.TrimSpace(req.CommandID),
|
||||
AdminUserID: actorID,
|
||||
TargetUserID: targetUserID,
|
||||
Role: role,
|
||||
Status: strings.TrimSpace(req.Status),
|
||||
Reason: strings.TrimSpace(req.Reason),
|
||||
})
|
||||
|
||||
@ -653,22 +653,30 @@ type bdProfile struct {
|
||||
ParentLeaderUserID int64
|
||||
}
|
||||
|
||||
// bdProfilesByUser 按角色取 active profile,BD 和 BD Leader 共用一张表但语义不同,调用方必须显式传 role。
|
||||
// bdProfilesByUser 按角色取 active profile,普通 BD 和 BD Leader 已拆成两张物理表,调用方必须显式传 role。
|
||||
func (s *Service) bdProfilesByUser(ctx context.Context, appCode string, userIDs []int64, role string) (map[int64]bdProfile, error) {
|
||||
userIDs = uniquePositiveUserIDs(userIDs)
|
||||
if len(userIDs) == 0 {
|
||||
return map[int64]bdProfile{}, nil
|
||||
}
|
||||
placeholders := placeholders(len(userIDs))
|
||||
args := make([]any, 0, len(userIDs)+2)
|
||||
args = append(args, appCode, role)
|
||||
args := make([]any, 0, len(userIDs)+1)
|
||||
args = append(args, appCode)
|
||||
for _, id := range userIDs {
|
||||
args = append(args, id)
|
||||
}
|
||||
rows, err := s.userDB.QueryContext(ctx, `
|
||||
query := `
|
||||
SELECT user_id, role, region_id, COALESCE(parent_leader_user_id, 0)
|
||||
FROM bd_profiles
|
||||
WHERE app_code = ? AND role = ? AND status = 'active' AND user_id IN (`+placeholders+`)`, args...)
|
||||
WHERE app_code = ? AND role = 'bd' AND status = 'active' AND user_id IN (` + placeholders + `)`
|
||||
if role == "bd_leader" {
|
||||
query = `
|
||||
SELECT user_id, 'bd_leader', region_id, 0
|
||||
FROM bd_leader_profiles
|
||||
WHERE app_code = ? AND status = 'active' AND user_id IN (` + placeholders + `)`
|
||||
}
|
||||
rows, err := s.userDB.QueryContext(ctx, `
|
||||
`+query, args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@ -157,8 +157,9 @@ func seedTeamSalaryFlow(t *testing.T, adminDB *sql.DB, userDB *sql.DB, walletDB
|
||||
('lalu', 3001, 4001, 686, 'active'),
|
||||
('lalu', 3002, 4001, 686, 'active')`,
|
||||
`INSERT INTO bd_profiles (app_code, user_id, role, region_id, parent_leader_user_id, status) VALUES
|
||||
('lalu', 4001, 'bd', 686, 5001, 'active'),
|
||||
('lalu', 5001, 'bd_leader', 686, 0, 'active')`,
|
||||
('lalu', 4001, 'bd', 686, 5001, 'active')`,
|
||||
`INSERT INTO bd_leader_profiles (app_code, user_id, region_id, status) VALUES
|
||||
('lalu', 5001, 686, 'active')`,
|
||||
)
|
||||
execAll(t, walletDB, fmt.Sprintf(`INSERT INTO host_salary_settlement_records
|
||||
(app_code, settlement_id, command_id, transaction_id, settlement_type, user_id, agency_owner_user_id,
|
||||
@ -255,6 +256,13 @@ func teamSalaryUserDDL() []string {
|
||||
status VARCHAR(24) NOT NULL,
|
||||
PRIMARY KEY (app_code, user_id, role)
|
||||
)`,
|
||||
`CREATE TABLE bd_leader_profiles (
|
||||
app_code VARCHAR(32) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
region_id BIGINT NOT NULL,
|
||||
status VARCHAR(24) NOT NULL,
|
||||
PRIMARY KEY (app_code, user_id)
|
||||
)`,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -4118,7 +4118,7 @@ func TestBDCenterOverviewUsesBDSalaryAndDirectAgencies(t *testing.T) {
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if hostClient.lastBD == nil || hostClient.lastBD.GetUserId() != 42 || hostClient.lastBDAgencies == nil || hostClient.lastBDAgencies.GetBdUserId() != 42 {
|
||||
if hostClient.lastBD == nil || hostClient.lastBD.GetUserId() != 42 || hostClient.lastBD.GetRole() != "bd" || hostClient.lastBDAgencies == nil || hostClient.lastBDAgencies.GetBdUserId() != 42 {
|
||||
t.Fatalf("bd center requests mismatch: profile=%+v agencies=%+v", hostClient.lastBD, hostClient.lastBDAgencies)
|
||||
}
|
||||
if walletClient.last == nil || walletClient.last.GetUserId() != 42 || len(walletClient.last.GetAssetTypes()) != 1 || walletClient.last.GetAssetTypes()[0] != "BD_SALARY_USD" {
|
||||
@ -4165,7 +4165,7 @@ func TestBDCenterInviteAgencyDirectlyForwardsInviteAgency(t *testing.T) {
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if hostClient.lastInviteAgency == nil || hostClient.lastInviteAgency.GetInviterUserId() != 42 || hostClient.lastInviteAgency.GetTargetUserId() != 99 || hostClient.lastInviteAgency.GetAgencyName() != "Target Agency" {
|
||||
if hostClient.lastBD == nil || hostClient.lastBD.GetRole() != "bd" || hostClient.lastInviteAgency == nil || hostClient.lastInviteAgency.GetInviterUserId() != 42 || hostClient.lastInviteAgency.GetTargetUserId() != 99 || hostClient.lastInviteAgency.GetAgencyName() != "Target Agency" {
|
||||
t.Fatalf("bd invite agency request mismatch: %+v", hostClient.lastInviteAgency)
|
||||
}
|
||||
var response httpkit.ResponseEnvelope
|
||||
@ -4179,6 +4179,47 @@ func TestBDCenterInviteAgencyDirectlyForwardsInviteAgency(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBDLeaderInviteBDForwardsSelfInvite(t *testing.T) {
|
||||
hostClient := &fakeUserHostClient{
|
||||
bdProfile: &userv1.BDProfile{UserId: 42, Role: "bd_leader", Status: "active", RegionId: 1001},
|
||||
inviteBDResp: &userv1.InviteBDResponse{Invitation: &userv1.RoleInvitation{
|
||||
InvitationId: 9002,
|
||||
CommandId: "cmd-bd-self",
|
||||
InvitationType: "bd",
|
||||
Status: "accepted",
|
||||
InviterUserId: 42,
|
||||
InviterBdUserId: 42,
|
||||
TargetUserId: 42,
|
||||
ParentLeaderUserId: 42,
|
||||
}},
|
||||
}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||
handler.SetUserHostClient(hostClient)
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/bd-leader/invitations/bd", bytes.NewReader([]byte(`{"command_id":"cmd-bd-self","target_user_id":"42"}`)))
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
request.Header.Set("X-Request-ID", "req-bd-self")
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
if hostClient.lastBD == nil || hostClient.lastBD.GetRole() != "bd_leader" || hostClient.lastInviteBD == nil || hostClient.lastInviteBD.GetInviterUserId() != 42 || hostClient.lastInviteBD.GetTargetUserId() != 42 {
|
||||
t.Fatalf("self invite must be forwarded to user-service: %+v", hostClient.lastInviteBD)
|
||||
}
|
||||
var response httpkit.ResponseEnvelope
|
||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("decode response failed: %v", err)
|
||||
}
|
||||
data := response.Data.(map[string]any)
|
||||
invitation := data["invitation"].(map[string]any)
|
||||
if invitation["status"] != "accepted" || invitation["target_user_id"] != "42" || invitation["parent_leader_user_id"] != "42" {
|
||||
t.Fatalf("bd self invite response mismatch: %+v", data)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgencyCenterOverviewRejectsNonOwner(t *testing.T) {
|
||||
hostClient := &fakeUserHostClient{roleSummary: &userv1.UserRoleSummary{UserId: 42, IsManager: false, AgencyId: 7001}}
|
||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{})
|
||||
|
||||
@ -395,6 +395,7 @@ func (h *Handler) resolveActiveBDLeader(writer http.ResponseWriter, request *htt
|
||||
resp, err := h.userHostClient.GetBDProfile(request.Context(), &userv1.GetBDProfileRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: userID,
|
||||
Role: "bd_leader",
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
@ -416,13 +417,14 @@ func (h *Handler) resolveActiveBD(writer http.ResponseWriter, request *http.Requ
|
||||
resp, err := h.userHostClient.GetBDProfile(request.Context(), &userv1.GetBDProfileRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: userID,
|
||||
Role: "bd",
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return nil, false
|
||||
}
|
||||
profile := resp.GetBdProfile()
|
||||
if profile == nil || profile.GetStatus() != bdStatusActive || (profile.GetRole() != "bd" && profile.GetRole() != "bd_leader") {
|
||||
if profile == nil || profile.GetStatus() != bdStatusActive || profile.GetRole() != "bd" {
|
||||
httpkit.WriteError(writer, request, http.StatusForbidden, httpkit.CodePermissionDenied, "permission denied")
|
||||
return nil, false
|
||||
}
|
||||
|
||||
@ -367,9 +367,14 @@ func (h *Handler) resolveAgencySalaryIdentity(writer http.ResponseWriter, reques
|
||||
}
|
||||
|
||||
func (h *Handler) resolveBDSalaryIdentity(writer http.ResponseWriter, request *http.Request, userID int64, identity string) (salaryWalletIdentityContext, bool) {
|
||||
role := "bd"
|
||||
if identity == salaryIdentityBDLeader {
|
||||
role = "bd_leader"
|
||||
}
|
||||
resp, err := h.userHostClient.GetBDProfile(request.Context(), &userv1.GetBDProfileRequest{
|
||||
Meta: httpkit.UserMeta(request, ""),
|
||||
UserId: userID,
|
||||
Role: role,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
|
||||
@ -750,6 +750,17 @@ CREATE TABLE IF NOT EXISTS bd_profiles (
|
||||
KEY idx_bd_profiles_parent_leader (app_code, parent_leader_user_id, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='BD资料表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS bd_leader_profiles (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||
user_id BIGINT NOT NULL PRIMARY KEY COMMENT '用户 ID',
|
||||
region_id BIGINT NOT NULL COMMENT '区域 ID',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
created_by_user_id BIGINT NOT NULL COMMENT '创建人用户 ID',
|
||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||
KEY idx_bd_leader_profiles_region_status (app_code, region_id, status)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='BD Leader资料表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS coin_seller_profiles (
|
||||
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
|
||||
user_id BIGINT NOT NULL PRIMARY KEY COMMENT '用户 ID',
|
||||
|
||||
@ -28,7 +28,7 @@ const (
|
||||
|
||||
// BDRoleBD 是普通拓展角色。
|
||||
BDRoleBD = "bd"
|
||||
// BDRoleLeader 是 BD Leader,具备邀请 BD 的能力,同时也是 BD。
|
||||
// BDRoleLeader 是 BD Leader,具备邀请 BD 的能力;它与普通 BD 身份分表保存。
|
||||
BDRoleLeader = "bd_leader"
|
||||
|
||||
// CoinSellerStatusActive 表示币商身份有效,可以使用币商金币账户转账能力。
|
||||
@ -124,7 +124,7 @@ const (
|
||||
ResultTypeMembership = "agency_membership"
|
||||
// ResultTypeInvitation 表示命令结果是 role_invitations 行。
|
||||
ResultTypeInvitation = "role_invitation"
|
||||
// ResultTypeBDProfile 表示命令结果是 bd_profiles 行。
|
||||
// ResultTypeBDProfile 表示命令结果是普通 BD 或 BD Leader 身份行。
|
||||
ResultTypeBDProfile = "bd_profile"
|
||||
// ResultTypeAgency 表示命令结果是 agencies 行。
|
||||
ResultTypeAgency = "agency"
|
||||
|
||||
@ -192,6 +192,7 @@ type SetBDStatusInput struct {
|
||||
CommandID string
|
||||
AdminUserID int64
|
||||
TargetUserID int64
|
||||
Role string
|
||||
Status string
|
||||
Reason string
|
||||
RequestID string
|
||||
@ -202,6 +203,7 @@ type SetBDStatusCommand struct {
|
||||
CommandID string
|
||||
AdminUserID int64
|
||||
TargetUserID int64
|
||||
Role string
|
||||
Status string
|
||||
Reason string
|
||||
RequestID string
|
||||
|
||||
@ -34,7 +34,7 @@ type Repository interface {
|
||||
DeleteAgency(ctx context.Context, command DeleteAgencyCommand) (hostdomain.Agency, error)
|
||||
SetAgencyJoinEnabled(ctx context.Context, command SetAgencyJoinEnabledCommand) (hostdomain.Agency, error)
|
||||
GetHostProfile(ctx context.Context, userID int64) (hostdomain.HostProfile, error)
|
||||
GetBDProfile(ctx context.Context, userID int64) (hostdomain.BDProfile, error)
|
||||
GetBDProfile(ctx context.Context, userID int64, role string) (hostdomain.BDProfile, error)
|
||||
GetCoinSellerProfile(ctx context.Context, userID int64) (hostdomain.CoinSellerProfile, error)
|
||||
ListActiveCoinSellersInMyRegion(ctx context.Context, userID int64) ([]hostdomain.CoinSellerListItem, error)
|
||||
GetUserRoleSummary(ctx context.Context, userID int64) (hostdomain.UserRoleSummary, error)
|
||||
@ -368,14 +368,15 @@ func (s *Service) CreateBD(ctx context.Context, command CreateBDInput) (hostdoma
|
||||
})
|
||||
}
|
||||
|
||||
// SetBDStatus 调整 BD 或 BD Leader 状态;Phase 2 只允许 active/disabled 两个稳定状态。
|
||||
// SetBDStatus 调整普通 BD 或 BD Leader 状态;调用方必须传 role,避免同一用户两种身份时误改。
|
||||
func (s *Service) SetBDStatus(ctx context.Context, command SetBDStatusInput) (hostdomain.BDProfile, error) {
|
||||
if err := s.requireWriteDependencies(); err != nil {
|
||||
return hostdomain.BDProfile{}, err
|
||||
}
|
||||
status := normalizeStatus(command.Status)
|
||||
if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.TargetUserID <= 0 {
|
||||
return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id and target_user_id are required")
|
||||
role := normalizeBDRole(command.Role)
|
||||
if strings.TrimSpace(command.CommandID) == "" || command.AdminUserID <= 0 || command.TargetUserID <= 0 || role == "" {
|
||||
return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "command_id, admin_user_id, target_user_id and role are required")
|
||||
}
|
||||
if status != hostdomain.BDStatusActive && status != hostdomain.BDStatusDisabled {
|
||||
return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "bd status is invalid")
|
||||
@ -386,6 +387,7 @@ func (s *Service) SetBDStatus(ctx context.Context, command SetBDStatusInput) (ho
|
||||
CommandID: strings.TrimSpace(command.CommandID),
|
||||
AdminUserID: command.AdminUserID,
|
||||
TargetUserID: command.TargetUserID,
|
||||
Role: role,
|
||||
Status: status,
|
||||
Reason: strings.TrimSpace(command.Reason),
|
||||
RequestID: strings.TrimSpace(command.RequestID),
|
||||
@ -548,16 +550,28 @@ func (s *Service) GetHostProfile(ctx context.Context, userID int64) (hostdomain.
|
||||
return s.repository.GetHostProfile(ctx, userID)
|
||||
}
|
||||
|
||||
// GetBDProfile 读取 BD/BD 负责人身份事实。
|
||||
func (s *Service) GetBDProfile(ctx context.Context, userID int64) (hostdomain.BDProfile, error) {
|
||||
// GetBDProfile 按指定 role 读取普通 BD 或 BD Leader 身份事实。
|
||||
func (s *Service) GetBDProfile(ctx context.Context, userID int64, role string) (hostdomain.BDProfile, error) {
|
||||
if s.repository == nil {
|
||||
return hostdomain.BDProfile{}, xerr.New(xerr.Unavailable, "host repository is not configured")
|
||||
}
|
||||
if userID <= 0 {
|
||||
return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "user_id is required")
|
||||
role = normalizeBDRole(role)
|
||||
if userID <= 0 || role == "" {
|
||||
return hostdomain.BDProfile{}, xerr.New(xerr.InvalidArgument, "user_id and role are required")
|
||||
}
|
||||
|
||||
return s.repository.GetBDProfile(ctx, userID)
|
||||
return s.repository.GetBDProfile(ctx, userID, role)
|
||||
}
|
||||
|
||||
func normalizeBDRole(role string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(role)) {
|
||||
case hostdomain.BDRoleBD:
|
||||
return hostdomain.BDRoleBD
|
||||
case hostdomain.BDRoleLeader:
|
||||
return hostdomain.BDRoleLeader
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// GetCoinSellerProfile 读取币商身份事实,供 gateway 在发起币商转账前校验。
|
||||
|
||||
@ -274,7 +274,7 @@ func TestAcceptBDInvitationDoesNotCreateHostProfile(t *testing.T) {
|
||||
if invitation.Status != hostdomain.InvitationStatusAccepted {
|
||||
t.Fatalf("bd invitation should be accepted immediately: %+v", invitation)
|
||||
}
|
||||
bdProfile, err := svc.GetBDProfile(ctx, 702)
|
||||
bdProfile, err := svc.GetBDProfile(ctx, 702, hostdomain.BDRoleBD)
|
||||
if err != nil {
|
||||
t.Fatalf("GetBDProfile failed: %v", err)
|
||||
}
|
||||
@ -286,6 +286,53 @@ func TestAcceptBDInvitationDoesNotCreateHostProfile(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInviteBDAcceptsSelfInviteForLeader(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
seedActiveUser(t, repository, 703, 10)
|
||||
repository.PutBDProfile(hostdomain.BDProfile{
|
||||
UserID: 703,
|
||||
Role: hostdomain.BDRoleLeader,
|
||||
RegionID: 10,
|
||||
Status: hostdomain.BDStatusActive,
|
||||
})
|
||||
svc := newHostService(repository, 3100, 4100)
|
||||
|
||||
invitation, err := svc.InviteBD(ctx, hostservice.InviteBDInput{
|
||||
CommandID: "invite-bd-self-703",
|
||||
InviterUserID: 703,
|
||||
TargetUserID: 703,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("bd leader self invite must be accepted: %v", err)
|
||||
}
|
||||
if invitation.Status != hostdomain.InvitationStatusAccepted || invitation.InviterUserID != 703 || invitation.TargetUserID != 703 || invitation.ParentLeaderUserID != 703 {
|
||||
t.Fatalf("self invite invitation mismatch: %+v", invitation)
|
||||
}
|
||||
ordinaryBD, err := svc.GetBDProfile(ctx, 703, hostdomain.BDRoleBD)
|
||||
if err != nil {
|
||||
t.Fatalf("self invite must create ordinary bd profile: %v", err)
|
||||
}
|
||||
if ordinaryBD.Role != hostdomain.BDRoleBD || ordinaryBD.ParentLeaderUserID != 703 {
|
||||
t.Fatalf("self invite ordinary bd mismatch: %+v", ordinaryBD)
|
||||
}
|
||||
leaderProfile, err := svc.GetBDProfile(ctx, 703, hostdomain.BDRoleLeader)
|
||||
if err != nil {
|
||||
t.Fatalf("self invite must keep existing leader profile unchanged: %v", err)
|
||||
}
|
||||
if leaderProfile.Role != hostdomain.BDRoleLeader || leaderProfile.Status != hostdomain.BDStatusActive {
|
||||
t.Fatalf("self invite leader profile mismatch: %+v", leaderProfile)
|
||||
}
|
||||
retried, err := svc.InviteBD(ctx, hostservice.InviteBDInput{
|
||||
CommandID: "invite-bd-self-703",
|
||||
InviterUserID: 703,
|
||||
TargetUserID: 703,
|
||||
})
|
||||
if err != nil || retried.InvitationID != invitation.InvitationID {
|
||||
t.Fatalf("self invite retry must replay existing invitation: retried=%+v err=%v", retried, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminCreateBDLeaderAndBDIdempotently(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
@ -327,6 +374,25 @@ func TestAdminCreateBDLeaderAndBDIdempotently(t *testing.T) {
|
||||
t.Fatalf("leader retry must replay existing result: %+v", retriedLeader)
|
||||
}
|
||||
|
||||
sameUserBD, err := svc.CreateBD(ctx, hostservice.CreateBDInput{
|
||||
CommandID: "admin-create-bd-801-same-user",
|
||||
AdminUserID: 1,
|
||||
TargetUserID: 801,
|
||||
ParentLeaderUserID: 801,
|
||||
Reason: "same user can also be bd",
|
||||
RequestID: "req-bd-same-user-1",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateBD for existing leader user failed: %v", err)
|
||||
}
|
||||
if sameUserBD.Role != hostdomain.BDRoleBD || sameUserBD.ParentLeaderUserID != 801 {
|
||||
t.Fatalf("same user bd mismatch: %+v", sameUserBD)
|
||||
}
|
||||
leaderAfterBD, err := svc.GetBDProfile(ctx, 801, hostdomain.BDRoleLeader)
|
||||
if err != nil || leaderAfterBD.Role != hostdomain.BDRoleLeader || leaderAfterBD.Status != hostdomain.BDStatusActive {
|
||||
t.Fatalf("ordinary bd creation must not rewrite leader row: leader=%+v err=%v", leaderAfterBD, err)
|
||||
}
|
||||
|
||||
bd, err := svc.CreateBD(ctx, hostservice.CreateBDInput{
|
||||
CommandID: "admin-create-bd-802",
|
||||
AdminUserID: 1,
|
||||
@ -360,6 +426,7 @@ func TestAdminCreateBDLeaderAndBDIdempotently(t *testing.T) {
|
||||
CommandID: "admin-disable-bd-802",
|
||||
AdminUserID: 1,
|
||||
TargetUserID: 802,
|
||||
Role: hostdomain.BDRoleBD,
|
||||
Status: hostdomain.BDStatusDisabled,
|
||||
Reason: "stop bd",
|
||||
RequestID: "req-bd-stop-1",
|
||||
@ -370,6 +437,52 @@ func TestAdminCreateBDLeaderAndBDIdempotently(t *testing.T) {
|
||||
if disabled.Status != hostdomain.BDStatusDisabled {
|
||||
t.Fatalf("disabled bd mismatch: %+v", disabled)
|
||||
}
|
||||
_, err = svc.InviteBD(ctx, hostservice.InviteBDInput{
|
||||
CommandID: "invite-disabled-bd-802-again",
|
||||
InviterUserID: 801,
|
||||
TargetUserID: 802,
|
||||
})
|
||||
if got := xerr.CodeOf(err); got != xerr.Conflict {
|
||||
t.Fatalf("disabled ordinary bd must not be invited again: got %s err=%v", got, err)
|
||||
}
|
||||
disabledSameUserBD, err := svc.SetBDStatus(ctx, hostservice.SetBDStatusInput{
|
||||
CommandID: "admin-disable-bd-801",
|
||||
AdminUserID: 1,
|
||||
TargetUserID: 801,
|
||||
Role: hostdomain.BDRoleBD,
|
||||
Status: hostdomain.BDStatusDisabled,
|
||||
Reason: "stop ordinary bd only",
|
||||
RequestID: "req-bd-stop-801",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SetBDStatus ordinary bd for same user failed: %v", err)
|
||||
}
|
||||
if disabledSameUserBD.Role != hostdomain.BDRoleBD || disabledSameUserBD.Status != hostdomain.BDStatusDisabled {
|
||||
t.Fatalf("disabled same user bd mismatch: %+v", disabledSameUserBD)
|
||||
}
|
||||
stillLeader, err := svc.GetBDProfile(ctx, 801, hostdomain.BDRoleLeader)
|
||||
if err != nil || stillLeader.Status != hostdomain.BDStatusActive {
|
||||
t.Fatalf("disabling ordinary bd must not affect leader: leader=%+v err=%v", stillLeader, err)
|
||||
}
|
||||
disabledLeader, err := svc.SetBDStatus(ctx, hostservice.SetBDStatusInput{
|
||||
CommandID: "admin-disable-leader-801",
|
||||
AdminUserID: 1,
|
||||
TargetUserID: 801,
|
||||
Role: hostdomain.BDRoleLeader,
|
||||
Status: hostdomain.BDStatusDisabled,
|
||||
Reason: "stop leader only",
|
||||
RequestID: "req-leader-stop-801",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SetBDStatus leader failed: %v", err)
|
||||
}
|
||||
if disabledLeader.Role != hostdomain.BDRoleLeader || disabledLeader.Status != hostdomain.BDStatusDisabled {
|
||||
t.Fatalf("disabled leader mismatch: %+v", disabledLeader)
|
||||
}
|
||||
ordinaryStillDisabled, err := svc.GetBDProfile(ctx, 801, hostdomain.BDRoleBD)
|
||||
if err != nil || ordinaryStillDisabled.Status != hostdomain.BDStatusDisabled {
|
||||
t.Fatalf("disabling leader must not rewrite ordinary bd: bd=%+v err=%v", ordinaryStillDisabled, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminCreateBDLeaderRejectsDisabledRegion(t *testing.T) {
|
||||
@ -808,8 +921,22 @@ func TestGetUserRoleSummaryAggregatesRolesAndPendingInvitations(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("GetUserRoleSummary failed: %v", err)
|
||||
}
|
||||
if !summary.IsHost || !summary.IsAgency || !summary.IsManager || !summary.IsBD || !summary.IsBDLeader || !summary.IsCoinSeller || summary.AgencyID != 933 || summary.BDID != 931 {
|
||||
t.Fatalf("role summary mismatch: %+v", summary)
|
||||
if !summary.IsHost || !summary.IsAgency || !summary.IsManager || summary.IsBD || !summary.IsBDLeader || !summary.IsCoinSeller || summary.AgencyID != 933 || summary.BDID != 931 {
|
||||
t.Fatalf("leader-only role summary mismatch: %+v", summary)
|
||||
}
|
||||
if _, err := svc.InviteBD(ctx, hostservice.InviteBDInput{
|
||||
CommandID: "invite-bd-self-931",
|
||||
InviterUserID: 931,
|
||||
TargetUserID: 931,
|
||||
}); err != nil {
|
||||
t.Fatalf("self InviteBD for summary failed: %v", err)
|
||||
}
|
||||
combinedSummary, err := svc.GetUserRoleSummary(ctx, 931)
|
||||
if err != nil {
|
||||
t.Fatalf("GetUserRoleSummary combined failed: %v", err)
|
||||
}
|
||||
if !combinedSummary.IsBD || !combinedSummary.IsBDLeader || combinedSummary.BDID != 931 {
|
||||
t.Fatalf("bd plus leader role summary mismatch: %+v", combinedSummary)
|
||||
}
|
||||
directBD, err := svc.GetUserRoleSummary(ctx, 932)
|
||||
if err != nil {
|
||||
|
||||
@ -24,7 +24,7 @@ func (r *Repository) CreateBDLeader(ctx context.Context, command hostservice.Cre
|
||||
if err := requireCommandResult(existing, hostdomain.CommandTypeCreateBDLeader, hostdomain.ResultTypeBDProfile); err != nil {
|
||||
return hostdomain.BDProfile{}, err
|
||||
}
|
||||
return queryBDProfile(ctx, tx, "WHERE user_id = ?", existing.ResultID)
|
||||
return queryBDLeaderProfile(ctx, tx, "WHERE user_id = ?", existing.ResultID)
|
||||
}
|
||||
|
||||
regionID, err := r.userRegion(ctx, tx, command.TargetUserID, "FOR UPDATE")
|
||||
@ -34,10 +34,10 @@ func (r *Repository) CreateBDLeader(ctx context.Context, command hostservice.Cre
|
||||
if err := requireActiveRegion(ctx, tx, regionID); err != nil {
|
||||
return hostdomain.BDProfile{}, err
|
||||
}
|
||||
if _, ok, err := queryBDProfileByUserMaybe(ctx, tx, command.TargetUserID, true); err != nil {
|
||||
if _, ok, err := queryBDLeaderProfileByUserMaybe(ctx, tx, command.TargetUserID, true); err != nil {
|
||||
return hostdomain.BDProfile{}, err
|
||||
} else if ok {
|
||||
return hostdomain.BDProfile{}, xerr.New(xerr.Conflict, "target already has bd profile")
|
||||
return hostdomain.BDProfile{}, xerr.New(xerr.Conflict, "target already has bd leader profile")
|
||||
}
|
||||
|
||||
profile := hostdomain.BDProfile{
|
||||
@ -49,7 +49,7 @@ func (r *Repository) CreateBDLeader(ctx context.Context, command hostservice.Cre
|
||||
CreatedAtMs: command.NowMs,
|
||||
UpdatedAtMs: command.NowMs,
|
||||
}
|
||||
if err := insertBDProfile(ctx, tx, profile); err != nil {
|
||||
if err := insertBDLeaderProfile(ctx, tx, profile); err != nil {
|
||||
return hostdomain.BDProfile{}, mapHostDuplicateError(err)
|
||||
}
|
||||
if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{CommandID: command.CommandID, CommandType: hostdomain.CommandTypeCreateBDLeader, ResultType: hostdomain.ResultTypeBDProfile, ResultID: profile.UserID, CreatedAtMs: command.NowMs}); err != nil {
|
||||
@ -86,12 +86,12 @@ func (r *Repository) CreateBD(ctx context.Context, command hostservice.CreateBDC
|
||||
return hostdomain.BDProfile{}, err
|
||||
}
|
||||
if command.ParentLeaderUserID > 0 {
|
||||
// 独立 BD 不需要父级行锁;只有传入 Leader 时才校验 Leader 身份、启用状态和区域归属。
|
||||
leader, err := queryBDProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.ParentLeaderUserID)
|
||||
// 独立 BD 不需要父级行锁;只有传入 Leader 时才锁定独立 Leader 表并校验启用状态和区域归属。
|
||||
leader, err := queryBDLeaderProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.ParentLeaderUserID)
|
||||
if err != nil {
|
||||
return hostdomain.BDProfile{}, err
|
||||
}
|
||||
if leader.Status != hostdomain.BDStatusActive || leader.Role != hostdomain.BDRoleLeader {
|
||||
if leader.Status != hostdomain.BDStatusActive {
|
||||
return hostdomain.BDProfile{}, xerr.New(xerr.PermissionDenied, "parent leader is not active")
|
||||
}
|
||||
if regionID != leader.RegionID {
|
||||
@ -129,7 +129,7 @@ func (r *Repository) CreateBD(ctx context.Context, command hostservice.CreateBDC
|
||||
return profile, nil
|
||||
}
|
||||
|
||||
// SetBDStatus 只改 BD 状态,不迁移角色、区域和上级负责人。
|
||||
// SetBDStatus 按调用方明确传入的角色只改对应身份状态,不迁移角色、区域和上级负责人。
|
||||
func (r *Repository) SetBDStatus(ctx context.Context, command hostservice.SetBDStatusCommand) (hostdomain.BDProfile, error) {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
@ -143,18 +143,37 @@ func (r *Repository) SetBDStatus(ctx context.Context, command hostservice.SetBDS
|
||||
if err := requireCommandResult(existing, hostdomain.CommandTypeSetBDStatus, hostdomain.ResultTypeBDProfile); err != nil {
|
||||
return hostdomain.BDProfile{}, err
|
||||
}
|
||||
if command.Role == hostdomain.BDRoleLeader {
|
||||
return queryBDLeaderProfile(ctx, tx, "WHERE user_id = ?", existing.ResultID)
|
||||
}
|
||||
return queryBDProfile(ctx, tx, "WHERE user_id = ?", existing.ResultID)
|
||||
}
|
||||
|
||||
before, err := queryBDProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.TargetUserID)
|
||||
var before hostdomain.BDProfile
|
||||
switch command.Role {
|
||||
case hostdomain.BDRoleBD:
|
||||
// 普通 BD 和 Leader 已经拆表;状态路由传 bd 时只锁普通 BD 行。
|
||||
before, err = queryBDProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.TargetUserID)
|
||||
case hostdomain.BDRoleLeader:
|
||||
// 状态路由传 bd_leader 时只锁 Leader 行,避免同用户的普通 BD 钱包权限被误停。
|
||||
before, err = queryBDLeaderProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.TargetUserID)
|
||||
default:
|
||||
err = xerr.New(xerr.InvalidArgument, "bd role is invalid")
|
||||
}
|
||||
if err != nil {
|
||||
return hostdomain.BDProfile{}, err
|
||||
}
|
||||
after := before
|
||||
after.Status = command.Status
|
||||
after.UpdatedAtMs = command.NowMs
|
||||
if err := updateBDProfileStatus(ctx, tx, after); err != nil {
|
||||
return hostdomain.BDProfile{}, err
|
||||
if command.Role == hostdomain.BDRoleLeader {
|
||||
if err := updateBDLeaderProfileStatus(ctx, tx, after); err != nil {
|
||||
return hostdomain.BDProfile{}, err
|
||||
}
|
||||
} else {
|
||||
if err := updateBDProfileStatus(ctx, tx, after); err != nil {
|
||||
return hostdomain.BDProfile{}, err
|
||||
}
|
||||
}
|
||||
if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{CommandID: command.CommandID, CommandType: hostdomain.CommandTypeSetBDStatus, ResultType: hostdomain.ResultTypeBDProfile, ResultID: after.UserID, CreatedAtMs: command.NowMs}); err != nil {
|
||||
return hostdomain.BDProfile{}, mapHostDuplicateError(err)
|
||||
@ -278,14 +297,11 @@ func (r *Repository) CreateAgency(ctx context.Context, command hostservice.Creat
|
||||
return hostdomain.CreateAgencyResult{}, err
|
||||
}
|
||||
if command.ParentBDUserID > 0 {
|
||||
// 独立 Agency 不需要父级行锁;只有传入 BD 时才校验 BD 启用状态和同区域归属。
|
||||
parentBD, err := queryBDProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.ParentBDUserID)
|
||||
// 独立 Agency 不需要父级行锁;传入父级时可挂普通 BD 或 Leader 自身,但必须是有效身份且同区域。
|
||||
parentBD, err := queryActiveAgencyInviterProfile(ctx, tx, command.ParentBDUserID)
|
||||
if err != nil {
|
||||
return hostdomain.CreateAgencyResult{}, err
|
||||
}
|
||||
if parentBD.Status != hostdomain.BDStatusActive {
|
||||
return hostdomain.CreateAgencyResult{}, xerr.New(xerr.PermissionDenied, "parent bd is not active")
|
||||
}
|
||||
if regionID != parentBD.RegionID {
|
||||
return hostdomain.CreateAgencyResult{}, xerr.New(xerr.PermissionDenied, "owner region does not match parent bd region")
|
||||
}
|
||||
|
||||
@ -47,6 +47,9 @@ const (
|
||||
bdProfileColumns = `
|
||||
user_id, role, region_id, COALESCE(parent_leader_user_id, 0), status,
|
||||
created_by_user_id, created_at_ms, updated_at_ms`
|
||||
bdLeaderProfileColumns = `
|
||||
user_id, 'bd_leader', region_id, 0, status,
|
||||
created_by_user_id, created_at_ms, updated_at_ms`
|
||||
coinSellerProfileColumns = `
|
||||
user_id, status, merchant_asset_type,
|
||||
created_by_user_id, created_at_ms, updated_at_ms`
|
||||
|
||||
@ -28,15 +28,12 @@ func (r *Repository) InviteAgency(ctx context.Context, command hostservice.Invit
|
||||
return queryRoleInvitation(ctx, tx, "WHERE invitation_id = ?", existing.ResultID)
|
||||
}
|
||||
|
||||
// 邀请人的 BD 身份行同时是授权凭证和邀请归属来源;
|
||||
// 锁住它可以避免 BD 被停用/换区时仍继续发出旧区域邀请。
|
||||
inviter, err := queryBDProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.InviterUserID)
|
||||
// InviteAgency 没有显式角色字段;普通 BD 行优先承载 BD Center 邀请,普通 BD 不可用时再使用 Leader 行承载负责人直属邀请。
|
||||
// 两种身份都用 FOR UPDATE 串行化停用/换区和邀请创建,避免旧权限在同一事务窗口内继续写入。
|
||||
inviter, err := queryActiveAgencyInviterProfile(ctx, tx, command.InviterUserID)
|
||||
if err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
}
|
||||
if inviter.Status != hostdomain.BDStatusActive {
|
||||
return hostdomain.RoleInvitation{}, xerr.New(xerr.PermissionDenied, "inviter bd is not active")
|
||||
}
|
||||
// 目标用户还不是 Host/Agency 时没有领域表行,所以锁 users 行来串行化目标身份变更。
|
||||
targetRegionID, err := r.userRegion(ctx, tx, command.TargetUserID, "FOR UPDATE")
|
||||
if err != nil {
|
||||
@ -59,7 +56,7 @@ func (r *Repository) InviteAgency(ctx context.Context, command hostservice.Invit
|
||||
}
|
||||
|
||||
// BD 发出的 Agency 邀请要记录直接上级 BD 和负责人链路;
|
||||
// 如果邀请人自己就是负责人,则 parent_leader_user_id 指向自己。
|
||||
// 如果本次以负责人身份邀请,则直接 BD 和负责人都指向 Leader 自己。
|
||||
parentLeaderUserID := inviter.ParentLeaderUserID
|
||||
if inviter.Role == hostdomain.BDRoleLeader {
|
||||
parentLeaderUserID = inviter.UserID
|
||||
@ -146,12 +143,12 @@ func (r *Repository) InviteBD(ctx context.Context, command hostservice.InviteBDC
|
||||
return queryRoleInvitation(ctx, tx, "WHERE invitation_id = ?", existing.ResultID)
|
||||
}
|
||||
|
||||
// 只有有效 BD 负责人能发展 BD;锁住负责人身份行让停用/降级和邀请创建串行。
|
||||
inviter, err := queryBDProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.InviterUserID)
|
||||
// 只有有效 BD 负责人能发展 BD;锁住独立 Leader 表行让停用和邀请创建串行。
|
||||
inviter, err := queryBDLeaderProfile(ctx, tx, "WHERE user_id = ? FOR UPDATE", command.InviterUserID)
|
||||
if err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
}
|
||||
if inviter.Status != hostdomain.BDStatusActive || inviter.Role != hostdomain.BDRoleLeader {
|
||||
if inviter.Status != hostdomain.BDStatusActive {
|
||||
return hostdomain.RoleInvitation{}, xerr.New(xerr.PermissionDenied, "inviter is not active bd leader")
|
||||
}
|
||||
// BD 从属于负责人的区域,邀请创建时先锁目标用户当前区域。
|
||||
@ -162,11 +159,12 @@ func (r *Repository) InviteBD(ctx context.Context, command hostservice.InviteBDC
|
||||
if targetRegionID != inviter.RegionID {
|
||||
return hostdomain.RoleInvitation{}, xerr.New(xerr.PermissionDenied, "target region does not match leader region")
|
||||
}
|
||||
// 目标已有有效 BD 身份行时不再创建新邀请,避免多条 BD 身份事实竞争。
|
||||
if _, ok, err := queryBDProfileMaybe(ctx, tx, command.TargetUserID, true); err != nil {
|
||||
// 目标已有普通 BD 身份行时不再创建新邀请,即使已停用也不能再插入第二条普通 BD 事实。
|
||||
// BD Leader 自邀也必须新增普通 BD 行,后续才能获得 BD_SALARY_USD 钱包鉴权。
|
||||
if _, ok, err := queryBDProfileByUserMaybe(ctx, tx, command.TargetUserID, true); err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
} else if ok {
|
||||
return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already has active bd profile")
|
||||
return hostdomain.RoleInvitation{}, xerr.New(xerr.Conflict, "target already has bd profile")
|
||||
}
|
||||
|
||||
invitation := hostdomain.RoleInvitation{
|
||||
@ -189,6 +187,15 @@ func (r *Repository) InviteBD(ctx context.Context, command hostservice.InviteBDC
|
||||
if err := insertRoleInvitation(ctx, tx, invitation); err != nil {
|
||||
return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
|
||||
}
|
||||
recordCommandResult := func() error {
|
||||
return insertCommandResult(ctx, tx, hostdomain.CommandResult{
|
||||
CommandID: command.CommandID,
|
||||
CommandType: hostdomain.CommandTypeInviteBD,
|
||||
ResultType: hostdomain.ResultTypeInvitation,
|
||||
ResultID: invitation.InvitationID,
|
||||
CreatedAtMs: command.NowMs,
|
||||
})
|
||||
}
|
||||
result := hostdomain.ProcessRoleInvitationResult{Invitation: invitation}
|
||||
if err := r.acceptBDInvitation(ctx, tx, hostservice.ProcessRoleInvitationCommand{
|
||||
CommandID: command.CommandID,
|
||||
@ -204,13 +211,7 @@ func (r *Repository) InviteBD(ctx context.Context, command hostservice.InviteBDC
|
||||
if err := insertHostOutbox(ctx, tx, command.EventID, "BDCreated", "bd_profile", invitation.TargetUserID, command.NowMs); err != nil {
|
||||
return hostdomain.RoleInvitation{}, err
|
||||
}
|
||||
if err := insertCommandResult(ctx, tx, hostdomain.CommandResult{
|
||||
CommandID: command.CommandID,
|
||||
CommandType: hostdomain.CommandTypeInviteBD,
|
||||
ResultType: hostdomain.ResultTypeInvitation,
|
||||
ResultID: invitation.InvitationID,
|
||||
CreatedAtMs: command.NowMs,
|
||||
}); err != nil {
|
||||
if err := recordCommandResult(); err != nil {
|
||||
return hostdomain.RoleInvitation{}, mapHostDuplicateError(err)
|
||||
}
|
||||
if err := tx.Commit(); err != nil {
|
||||
@ -220,6 +221,16 @@ func (r *Repository) InviteBD(ctx context.Context, command hostservice.InviteBDC
|
||||
return invitation, nil
|
||||
}
|
||||
|
||||
func queryActiveAgencyInviterProfile(ctx context.Context, tx *sql.Tx, userID int64) (hostdomain.BDProfile, error) {
|
||||
if profile, ok, err := queryBDProfileMaybe(ctx, tx, userID, true); err != nil || ok {
|
||||
return profile, err
|
||||
}
|
||||
if profile, ok, err := queryBDLeaderProfileMaybe(ctx, tx, userID, true); err != nil || ok {
|
||||
return profile, err
|
||||
}
|
||||
return hostdomain.BDProfile{}, xerr.New(xerr.PermissionDenied, "inviter bd is not active")
|
||||
}
|
||||
|
||||
// ProcessRoleInvitation 处理角色邀请,接受路径在同一事务内创建 Agency/BD 角色事实。
|
||||
func (r *Repository) ProcessRoleInvitation(ctx context.Context, command hostservice.ProcessRoleInvitationCommand) (hostdomain.ProcessRoleInvitationResult, error) {
|
||||
tx, err := r.db.BeginTx(ctx, nil)
|
||||
@ -397,11 +408,11 @@ func (r *Repository) acceptBDInvitation(ctx context.Context, tx *sql.Tx, command
|
||||
if regionID != invitation.RegionID {
|
||||
return xerr.New(xerr.PermissionDenied, "target region no longer matches invitation")
|
||||
}
|
||||
// BD 身份是独立角色事实,不依赖 host_profile;只需要阻止重复有效 BD。
|
||||
if _, ok, err := queryBDProfileMaybe(ctx, tx, invitation.TargetUserID, true); err != nil {
|
||||
// BD 身份是独立角色事实,不依赖 host_profile;任何已有普通 BD 行都不能重复插入。
|
||||
if _, ok, err := queryBDProfileByUserMaybe(ctx, tx, invitation.TargetUserID, true); err != nil {
|
||||
return err
|
||||
} else if ok {
|
||||
return xerr.New(xerr.Conflict, "target already has active bd profile")
|
||||
return xerr.New(xerr.Conflict, "target already has bd profile")
|
||||
}
|
||||
bd := hostdomain.BDProfile{
|
||||
UserID: invitation.TargetUserID,
|
||||
|
||||
@ -100,8 +100,11 @@ func (r *Repository) GetHostProfile(ctx context.Context, userID int64) (hostdoma
|
||||
return queryHostProfile(ctx, r.db, "WHERE user_id = ?", userID)
|
||||
}
|
||||
|
||||
// GetBDProfile 读取 BD 身份事实。
|
||||
func (r *Repository) GetBDProfile(ctx context.Context, userID int64) (hostdomain.BDProfile, error) {
|
||||
// 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)
|
||||
}
|
||||
|
||||
@ -201,8 +204,9 @@ func (r *Repository) GetUserRoleSummary(ctx context.Context, userID int64) (host
|
||||
var hostAgencyID int64
|
||||
var ownerAgencyID int64
|
||||
var bdUserID int64
|
||||
var bdRole string
|
||||
var bdStatus string
|
||||
var leaderUserID int64
|
||||
var leaderStatus string
|
||||
var coinSellerStatus string
|
||||
var merchantAssetType string
|
||||
|
||||
@ -213,8 +217,9 @@ func (r *Repository) GetUserRoleSummary(ctx context.Context, userID int64) (host
|
||||
COALESCE(h.current_agency_id, 0),
|
||||
COALESCE(a.agency_id, 0),
|
||||
COALESCE(b.user_id, 0),
|
||||
COALESCE(b.role, ''),
|
||||
COALESCE(b.status, ''),
|
||||
COALESCE(bl.user_id, 0),
|
||||
COALESCE(bl.status, ''),
|
||||
COALESCE(c.status, ''),
|
||||
COALESCE(c.merchant_asset_type, '')
|
||||
FROM (SELECT ? AS user_id) u
|
||||
@ -224,6 +229,8 @@ func (r *Repository) GetUserRoleSummary(ctx context.Context, userID int64) (host
|
||||
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 coin_seller_profiles c
|
||||
ON c.app_code = ? AND c.user_id = u.user_id`,
|
||||
userID,
|
||||
@ -231,7 +238,8 @@ func (r *Repository) GetUserRoleSummary(ctx context.Context, userID int64) (host
|
||||
app, hostdomain.AgencyStatusActive,
|
||||
app,
|
||||
app,
|
||||
).Scan(&hostStatus, &hostSource, &hostAgencyID, &ownerAgencyID, &bdUserID, &bdRole, &bdStatus, &coinSellerStatus, &merchantAssetType)
|
||||
app,
|
||||
).Scan(&hostStatus, &hostSource, &hostAgencyID, &ownerAgencyID, &bdUserID, &bdStatus, &leaderUserID, &leaderStatus, &coinSellerStatus, &merchantAssetType)
|
||||
if err != nil {
|
||||
return hostdomain.UserRoleSummary{}, err
|
||||
}
|
||||
@ -249,13 +257,16 @@ func (r *Repository) GetUserRoleSummary(ctx context.Context, userID int64) (host
|
||||
summary.BDStatus = bdStatus
|
||||
if bdUserID > 0 && bdStatus == hostdomain.BDStatusActive {
|
||||
summary.BDID = bdUserID
|
||||
switch bdRole {
|
||||
case hostdomain.BDRoleLeader:
|
||||
summary.IsBD = true
|
||||
summary.IsBDLeader = true
|
||||
case hostdomain.BDRoleBD:
|
||||
summary.IsBD = true
|
||||
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
|
||||
@ -281,13 +292,13 @@ func (r *Repository) HasActiveAgencyOwner(ctx context.Context, userID int64) (in
|
||||
return agency.AgencyID, true, nil
|
||||
}
|
||||
|
||||
// ListBDLeaderBDs 读取 BD Leader 直属 BD;调用前先校验 leader 当前仍有效,避免停用后继续读取团队数据。
|
||||
// ListBDLeaderBDs 读取 BD Leader 直属普通 BD;调用前先校验独立 Leader 行当前仍有效,避免停用后继续读取团队数据。
|
||||
func (r *Repository) ListBDLeaderBDs(ctx context.Context, command hostservice.ListBDLeaderBDsCommand) ([]hostdomain.BDProfile, error) {
|
||||
leader, err := queryBDProfile(ctx, r.db, "WHERE user_id = ?", command.LeaderUserID)
|
||||
leader, err := queryBDLeaderProfile(ctx, r.db, "WHERE user_id = ?", command.LeaderUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if leader.Status != hostdomain.BDStatusActive || leader.Role != hostdomain.BDRoleLeader {
|
||||
if leader.Status != hostdomain.BDStatusActive {
|
||||
return nil, xerr.New(xerr.PermissionDenied, "leader is not active")
|
||||
}
|
||||
status := command.Status
|
||||
@ -298,13 +309,13 @@ func (r *Repository) ListBDLeaderBDs(ctx context.Context, command hostservice.Li
|
||||
return queryBDProfiles(ctx, r.db, clause, args...)
|
||||
}
|
||||
|
||||
// ListBDLeaderAgencies 读取 BD Leader 团队 Agency;包含直属 leader 自己创建的 Agency 和下属 BD 创建的 Agency。
|
||||
// ListBDLeaderAgencies 读取 BD Leader 团队 Agency;包含直属 leader 自己创建的 Agency 和下属普通 BD 创建的 Agency。
|
||||
func (r *Repository) ListBDLeaderAgencies(ctx context.Context, command hostservice.ListBDLeaderAgenciesCommand) ([]hostdomain.Agency, error) {
|
||||
leader, err := queryBDProfile(ctx, r.db, "WHERE user_id = ?", command.LeaderUserID)
|
||||
leader, err := queryBDLeaderProfile(ctx, r.db, "WHERE user_id = ?", command.LeaderUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if leader.Status != hostdomain.BDStatusActive || leader.Role != hostdomain.BDRoleLeader {
|
||||
if leader.Status != hostdomain.BDStatusActive {
|
||||
return nil, xerr.New(xerr.PermissionDenied, "leader is not active")
|
||||
}
|
||||
status := command.Status
|
||||
@ -325,13 +336,13 @@ func (r *Repository) ListBDLeaderAgencies(ctx context.Context, command hostservi
|
||||
return queryAgencies(ctx, r.db, clause, args...)
|
||||
}
|
||||
|
||||
// ListBDAgencies 读取一个有效 BD 直接拓展的 Agency,不包含同负责人下其他 BD 的数据。
|
||||
// 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 {
|
||||
if bd.Status != hostdomain.BDStatusActive || bd.Role != hostdomain.BDRoleBD {
|
||||
return nil, xerr.New(xerr.PermissionDenied, "bd is not active")
|
||||
}
|
||||
status := command.Status
|
||||
@ -558,6 +569,15 @@ func queryBDProfiles(ctx context.Context, q sqlQueryer, clause string, args ...a
|
||||
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 {
|
||||
@ -574,6 +594,22 @@ func queryBDProfileMaybe(ctx context.Context, q sqlQueryer, userID int64, lock b
|
||||
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 {
|
||||
@ -591,6 +627,23 @@ func queryBDProfileByUserMaybe(ctx context.Context, q sqlQueryer, userID int64,
|
||||
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(
|
||||
|
||||
@ -148,7 +148,7 @@ func updateAgencyAdminState(ctx context.Context, tx *sql.Tx, agency hostdomain.A
|
||||
}
|
||||
|
||||
func insertBDProfile(ctx context.Context, tx *sql.Tx, profile hostdomain.BDProfile) error {
|
||||
// BD 身份行和 host_profile 相互独立;BD 用户不会自动成为 Host。
|
||||
// 普通 BD 身份行和 host_profile 相互独立;BD 用户不会自动成为 Host,也不会隐式拥有 BD Leader 权限。
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO bd_profiles (
|
||||
app_code, user_id, role, region_id, parent_leader_user_id, status,
|
||||
@ -158,11 +158,32 @@ func insertBDProfile(ctx context.Context, tx *sql.Tx, profile hostdomain.BDProfi
|
||||
return err
|
||||
}
|
||||
|
||||
func insertBDLeaderProfile(ctx context.Context, tx *sql.Tx, profile hostdomain.BDProfile) error {
|
||||
// BD Leader 使用独立表承载负责人身份,避免和普通 BD 工资、状态、团队归属混成同一条事实。
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
INSERT INTO bd_leader_profiles (
|
||||
app_code, user_id, region_id, status,
|
||||
created_by_user_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`, appcode.FromContext(ctx), profile.UserID, profile.RegionID, profile.Status, profile.CreatedByUserID, profile.CreatedAtMs, profile.UpdatedAtMs)
|
||||
return err
|
||||
}
|
||||
|
||||
func updateBDProfileStatus(ctx context.Context, tx *sql.Tx, profile hostdomain.BDProfile) error {
|
||||
// 状态变更只更新 status/updated_at_ms,不改角色、区域和上级 Leader 这些身份历史字段。
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
UPDATE bd_profiles
|
||||
SET status = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND user_id = ? AND role = 'bd'
|
||||
`, profile.Status, profile.UpdatedAtMs, appcode.FromContext(ctx), profile.UserID)
|
||||
return err
|
||||
}
|
||||
|
||||
func updateBDLeaderProfileStatus(ctx context.Context, tx *sql.Tx, profile hostdomain.BDProfile) error {
|
||||
// Leader 状态只影响负责人权限和 ADMIN_SALARY_USD 钱包入口,不影响同用户可能存在的普通 BD 身份。
|
||||
_, err := tx.ExecContext(ctx, `
|
||||
UPDATE bd_leader_profiles
|
||||
SET status = ?, updated_at_ms = ?
|
||||
WHERE app_code = ? AND user_id = ?
|
||||
`, profile.Status, profile.UpdatedAtMs, appcode.FromContext(ctx), profile.UserID)
|
||||
return err
|
||||
|
||||
@ -634,7 +634,7 @@ func (r *Repository) PutHostProfile(profile hostdomain.HostProfile) {
|
||||
}
|
||||
}
|
||||
|
||||
// PutBDProfile seeds a BD or BD Leader profile fact for host-domain tests.
|
||||
// PutBDProfile seeds ordinary BD rows into bd_profiles and BD Leader rows into bd_leader_profiles.
|
||||
func (r *Repository) PutBDProfile(profile hostdomain.BDProfile) {
|
||||
r.t.Helper()
|
||||
|
||||
@ -653,6 +653,22 @@ func (r *Repository) PutBDProfile(profile hostdomain.BDProfile) {
|
||||
}
|
||||
profile.UpdatedAtMs = nowMs
|
||||
|
||||
if profile.Role == hostdomain.BDRoleLeader {
|
||||
_, err := r.schema.DB.ExecContext(context.Background(), `
|
||||
INSERT INTO bd_leader_profiles (
|
||||
user_id, region_id, status, created_by_user_id, created_at_ms, updated_at_ms
|
||||
) VALUES (?, ?, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
region_id = VALUES(region_id),
|
||||
status = VALUES(status),
|
||||
updated_at_ms = VALUES(updated_at_ms)
|
||||
`, profile.UserID, profile.RegionID, profile.Status, profile.CreatedByUserID, profile.CreatedAtMs, profile.UpdatedAtMs)
|
||||
if err != nil {
|
||||
r.t.Fatalf("seed bd leader profile %d failed: %v", profile.UserID, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
_, err := r.schema.DB.ExecContext(context.Background(), `
|
||||
INSERT INTO bd_profiles (
|
||||
user_id, role, region_id, parent_leader_user_id, status,
|
||||
|
||||
@ -0,0 +1,174 @@
|
||||
package grpc_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
userv1 "hyapp.local/api/proto/user/v1"
|
||||
"hyapp/pkg/xerr"
|
||||
hostdomain "hyapp/services/user-service/internal/domain/host"
|
||||
userdomain "hyapp/services/user-service/internal/domain/user"
|
||||
authservice "hyapp/services/user-service/internal/service/auth"
|
||||
hostservice "hyapp/services/user-service/internal/service/host"
|
||||
"hyapp/services/user-service/internal/testutil/mysqltest"
|
||||
grpcserver "hyapp/services/user-service/internal/transport/grpc"
|
||||
)
|
||||
|
||||
type grpcHostSequenceIDGenerator struct {
|
||||
next int64
|
||||
}
|
||||
|
||||
func (g *grpcHostSequenceIDGenerator) NewInt64() int64 {
|
||||
id := g.next
|
||||
g.next++
|
||||
return id
|
||||
}
|
||||
|
||||
func TestBDLeaderSelfInviteFlowThroughGRPCUsesSplitIdentityTables(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
repository.PutRegion(userdomain.Region{RegionID: 686, RegionCode: "BD-RPC", Name: "BD RPC Region"})
|
||||
seedActiveRPCUser(t, repository, 10001, 686)
|
||||
|
||||
hostSvc := hostservice.New(repository.HostRepository(),
|
||||
hostservice.WithIDGenerator(&grpcHostSequenceIDGenerator{next: 700000}),
|
||||
hostservice.WithClock(func() time.Time { return time.UnixMilli(1700000000000) }),
|
||||
)
|
||||
server := grpcserver.NewServer(authservice.New(authservice.Config{}), newUserService(repository), hostSvc)
|
||||
meta := &userv1.RequestMeta{AppCode: "lalu", RequestId: "req-bd-grpc-flow"}
|
||||
|
||||
leaderResp, err := server.CreateBDLeader(ctx, &userv1.CreateBDLeaderRequest{
|
||||
Meta: meta,
|
||||
CommandId: "rpc-create-leader-10001",
|
||||
AdminUserId: 90001,
|
||||
TargetUserId: 10001,
|
||||
Reason: "grpc split identity flow",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("CreateBDLeader RPC failed: %v", err)
|
||||
}
|
||||
if leaderResp.GetBdProfile().GetRole() != hostdomain.BDRoleLeader || leaderResp.GetBdProfile().GetStatus() != hostdomain.BDStatusActive {
|
||||
t.Fatalf("leader response mismatch: %+v", leaderResp.GetBdProfile())
|
||||
}
|
||||
|
||||
if _, err := server.GetBDProfile(ctx, &userv1.GetBDProfileRequest{Meta: meta, UserId: 10001, Role: hostdomain.BDRoleBD}); xerr.ReasonFromGRPC(err) != xerr.NotFound {
|
||||
t.Fatalf("leader-only user must not have ordinary bd before self invite: err=%v", err)
|
||||
}
|
||||
|
||||
inviteResp, err := server.InviteBD(ctx, &userv1.InviteBDRequest{
|
||||
Meta: meta,
|
||||
CommandId: "rpc-self-invite-bd-10001",
|
||||
InviterUserId: 10001,
|
||||
TargetUserId: 10001,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("InviteBD self RPC failed: %v", err)
|
||||
}
|
||||
invitation := inviteResp.GetInvitation()
|
||||
if invitation.GetStatus() != hostdomain.InvitationStatusAccepted || invitation.GetTargetUserId() != 10001 || invitation.GetParentLeaderUserId() != 10001 {
|
||||
t.Fatalf("self invite response mismatch: %+v", invitation)
|
||||
}
|
||||
retryInviteResp, err := server.InviteBD(ctx, &userv1.InviteBDRequest{
|
||||
Meta: meta,
|
||||
CommandId: "rpc-self-invite-bd-10001",
|
||||
InviterUserId: 10001,
|
||||
TargetUserId: 10001,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("InviteBD retry RPC failed: %v", err)
|
||||
}
|
||||
if retryInviteResp.GetInvitation().GetInvitationId() != invitation.GetInvitationId() {
|
||||
t.Fatalf("self invite retry must replay same invitation: first=%+v retry=%+v", invitation, retryInviteResp.GetInvitation())
|
||||
}
|
||||
|
||||
bdResp, err := server.GetBDProfile(ctx, &userv1.GetBDProfileRequest{Meta: meta, UserId: 10001, Role: hostdomain.BDRoleBD})
|
||||
if err != nil {
|
||||
t.Fatalf("GetBDProfile role=bd RPC failed after self invite: %v", err)
|
||||
}
|
||||
if bdResp.GetBdProfile().GetRole() != hostdomain.BDRoleBD || bdResp.GetBdProfile().GetParentLeaderUserId() != 10001 {
|
||||
t.Fatalf("ordinary bd profile mismatch: %+v", bdResp.GetBdProfile())
|
||||
}
|
||||
leaderCheckResp, err := server.GetBDProfile(ctx, &userv1.GetBDProfileRequest{Meta: meta, UserId: 10001, Role: hostdomain.BDRoleLeader})
|
||||
if err != nil {
|
||||
t.Fatalf("GetBDProfile role=bd_leader RPC failed: %v", err)
|
||||
}
|
||||
if leaderCheckResp.GetBdProfile().GetRole() != hostdomain.BDRoleLeader || leaderCheckResp.GetBdProfile().GetStatus() != hostdomain.BDStatusActive {
|
||||
t.Fatalf("leader profile changed unexpectedly: %+v", leaderCheckResp.GetBdProfile())
|
||||
}
|
||||
|
||||
summaryResp, err := server.GetUserRoleSummary(ctx, &userv1.GetUserRoleSummaryRequest{Meta: meta, UserId: 10001})
|
||||
if err != nil {
|
||||
t.Fatalf("GetUserRoleSummary RPC failed: %v", err)
|
||||
}
|
||||
if !summaryResp.GetSummary().GetIsBd() || !summaryResp.GetSummary().GetIsBdLeader() {
|
||||
t.Fatalf("summary must expose both independent identities: %+v", summaryResp.GetSummary())
|
||||
}
|
||||
|
||||
disabledBDResp, err := server.SetBDStatus(ctx, &userv1.SetBDStatusRequest{
|
||||
Meta: meta,
|
||||
CommandId: "rpc-disable-bd-10001",
|
||||
AdminUserId: 90001,
|
||||
TargetUserId: 10001,
|
||||
Role: hostdomain.BDRoleBD,
|
||||
Status: hostdomain.BDStatusDisabled,
|
||||
Reason: "disable ordinary bd only",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SetBDStatus role=bd RPC failed: %v", err)
|
||||
}
|
||||
if disabledBDResp.GetBdProfile().GetRole() != hostdomain.BDRoleBD || disabledBDResp.GetBdProfile().GetStatus() != hostdomain.BDStatusDisabled {
|
||||
t.Fatalf("disabled ordinary bd response mismatch: %+v", disabledBDResp.GetBdProfile())
|
||||
}
|
||||
leaderStillActiveResp, err := server.GetBDProfile(ctx, &userv1.GetBDProfileRequest{Meta: meta, UserId: 10001, Role: hostdomain.BDRoleLeader})
|
||||
if err != nil {
|
||||
t.Fatalf("GetBDProfile role=bd_leader after bd disable failed: %v", err)
|
||||
}
|
||||
if leaderStillActiveResp.GetBdProfile().GetStatus() != hostdomain.BDStatusActive {
|
||||
t.Fatalf("disabling ordinary bd must not affect leader: %+v", leaderStillActiveResp.GetBdProfile())
|
||||
}
|
||||
|
||||
disabledLeaderResp, err := server.SetBDStatus(ctx, &userv1.SetBDStatusRequest{
|
||||
Meta: meta,
|
||||
CommandId: "rpc-disable-leader-10001",
|
||||
AdminUserId: 90001,
|
||||
TargetUserId: 10001,
|
||||
Role: hostdomain.BDRoleLeader,
|
||||
Status: hostdomain.BDStatusDisabled,
|
||||
Reason: "disable leader only",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("SetBDStatus role=bd_leader RPC failed: %v", err)
|
||||
}
|
||||
if disabledLeaderResp.GetBdProfile().GetRole() != hostdomain.BDRoleLeader || disabledLeaderResp.GetBdProfile().GetStatus() != hostdomain.BDStatusDisabled {
|
||||
t.Fatalf("disabled leader response mismatch: %+v", disabledLeaderResp.GetBdProfile())
|
||||
}
|
||||
|
||||
var bdRows int
|
||||
var leaderRows int
|
||||
if err := repository.RawDB().QueryRowContext(ctx, `SELECT COUNT(1) FROM bd_profiles WHERE app_code = 'lalu' AND user_id = 10001 AND role = 'bd'`).Scan(&bdRows); err != nil {
|
||||
t.Fatalf("count ordinary bd rows failed: %v", err)
|
||||
}
|
||||
if err := repository.RawDB().QueryRowContext(ctx, `SELECT COUNT(1) FROM bd_leader_profiles WHERE app_code = 'lalu' AND user_id = 10001`).Scan(&leaderRows); err != nil {
|
||||
t.Fatalf("count leader rows failed: %v", err)
|
||||
}
|
||||
if bdRows != 1 || leaderRows != 1 {
|
||||
t.Fatalf("split identity rows mismatch: bd_rows=%d leader_rows=%d", bdRows, leaderRows)
|
||||
}
|
||||
}
|
||||
|
||||
func seedActiveRPCUser(t *testing.T, repository *mysqltest.Repository, userID int64, regionID int64) {
|
||||
t.Helper()
|
||||
repository.PutUser(userdomain.User{
|
||||
UserID: userID,
|
||||
DefaultDisplayUserID: "910001",
|
||||
CurrentDisplayUserID: "910001",
|
||||
CurrentDisplayUserIDKind: userdomain.DisplayUserIDKindDefault,
|
||||
Country: "CN",
|
||||
RegionID: regionID,
|
||||
ProfileCompleted: true,
|
||||
ProfileCompletedAtMs: 1,
|
||||
OnboardingStatus: userdomain.OnboardingStatusCompleted,
|
||||
Status: userdomain.StatusActive,
|
||||
})
|
||||
}
|
||||
@ -124,7 +124,7 @@ func (s *Server) InviteBD(ctx context.Context, req *userv1.InviteBDRequest) (*us
|
||||
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured"))
|
||||
}
|
||||
ctx = contextWithApp(ctx, req.GetMeta())
|
||||
// BD 负责人授权在持久化层内基于 bd_profiles 当前事实判断。
|
||||
// BD 负责人授权在持久化层内基于 bd_leader_profiles 当前事实判断。
|
||||
invitation, err := s.hostSvc.InviteBD(ctx, hostservice.InviteBDInput{
|
||||
CommandID: req.GetCommandId(),
|
||||
InviterUserID: req.GetInviterUserId(),
|
||||
@ -244,13 +244,13 @@ func (s *Server) GetHostProfile(ctx context.Context, req *userv1.GetHostProfileR
|
||||
return &userv1.GetHostProfileResponse{HostProfile: toProtoHostProfile(profile)}, nil
|
||||
}
|
||||
|
||||
// GetBDProfile 返回当前用户 BD/BD 负责人身份事实。
|
||||
// GetBDProfile 按调用方声明的角色返回普通 BD 或 BD Leader 身份事实。
|
||||
func (s *Server) GetBDProfile(ctx context.Context, req *userv1.GetBDProfileRequest) (*userv1.GetBDProfileResponse, error) {
|
||||
if s.hostSvc == nil {
|
||||
return nil, xerr.ToGRPCError(xerr.New(xerr.Unavailable, "host service is not configured"))
|
||||
}
|
||||
ctx = contextWithApp(ctx, req.GetMeta())
|
||||
profile, err := s.hostSvc.GetBDProfile(ctx, req.GetUserId())
|
||||
profile, err := s.hostSvc.GetBDProfile(ctx, req.GetUserId(), req.GetRole())
|
||||
if err != nil {
|
||||
return nil, xerr.ToGRPCError(err)
|
||||
}
|
||||
|
||||
@ -57,6 +57,7 @@ func (s *Server) SetBDStatus(ctx context.Context, req *userv1.SetBDStatusRequest
|
||||
CommandID: req.GetCommandId(),
|
||||
AdminUserID: req.GetAdminUserId(),
|
||||
TargetUserID: req.GetTargetUserId(),
|
||||
Role: req.GetRole(),
|
||||
Status: req.GetStatus(),
|
||||
Reason: req.GetReason(),
|
||||
RequestID: req.GetMeta().GetRequestId(),
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user