撤销资源组
This commit is contained in:
parent
9c547faf0e
commit
6fd0f03895
File diff suppressed because it is too large
Load Diff
@ -494,6 +494,9 @@ message ResourceGrant {
|
|||||||
repeated ResourceGrantItem items = 11;
|
repeated ResourceGrantItem items = 11;
|
||||||
int64 created_at_ms = 12;
|
int64 created_at_ms = 12;
|
||||||
int64 updated_at_ms = 13;
|
int64 updated_at_ms = 13;
|
||||||
|
int64 revoked_at_ms = 14;
|
||||||
|
int64 revoked_by_user_id = 15;
|
||||||
|
string revoke_reason = 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
message ResourceGroupItemInput {
|
message ResourceGroupItemInput {
|
||||||
@ -793,6 +796,14 @@ message GrantResourceGroupRequest {
|
|||||||
string grant_source = 7;
|
string grant_source = 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message RevokeResourceGrantRequest {
|
||||||
|
string request_id = 1;
|
||||||
|
string app_code = 2;
|
||||||
|
string grant_id = 3;
|
||||||
|
string reason = 4;
|
||||||
|
int64 operator_user_id = 5;
|
||||||
|
}
|
||||||
|
|
||||||
message ResourceGrantResponse {
|
message ResourceGrantResponse {
|
||||||
ResourceGrant grant = 1;
|
ResourceGrant grant = 1;
|
||||||
}
|
}
|
||||||
@ -1989,6 +2000,7 @@ service WalletService {
|
|||||||
rpc UpsertGiftTypeConfig(UpsertGiftTypeConfigRequest) returns (GiftTypeConfigResponse);
|
rpc UpsertGiftTypeConfig(UpsertGiftTypeConfigRequest) returns (GiftTypeConfigResponse);
|
||||||
rpc GrantResource(GrantResourceRequest) returns (ResourceGrantResponse);
|
rpc GrantResource(GrantResourceRequest) returns (ResourceGrantResponse);
|
||||||
rpc GrantResourceGroup(GrantResourceGroupRequest) returns (ResourceGrantResponse);
|
rpc GrantResourceGroup(GrantResourceGroupRequest) returns (ResourceGrantResponse);
|
||||||
|
rpc RevokeResourceGrant(RevokeResourceGrantRequest) returns (ResourceGrantResponse);
|
||||||
rpc ListUserResources(ListUserResourcesRequest) returns (ListUserResourcesResponse);
|
rpc ListUserResources(ListUserResourcesRequest) returns (ListUserResourcesResponse);
|
||||||
rpc EquipUserResource(EquipUserResourceRequest) returns (EquipUserResourceResponse);
|
rpc EquipUserResource(EquipUserResourceRequest) returns (EquipUserResourceResponse);
|
||||||
rpc UnequipUserResource(UnequipUserResourceRequest) returns (UnequipUserResourceResponse);
|
rpc UnequipUserResource(UnequipUserResourceRequest) returns (UnequipUserResourceResponse);
|
||||||
|
|||||||
@ -232,6 +232,7 @@ const (
|
|||||||
WalletService_UpsertGiftTypeConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpsertGiftTypeConfig"
|
WalletService_UpsertGiftTypeConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpsertGiftTypeConfig"
|
||||||
WalletService_GrantResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResource"
|
WalletService_GrantResource_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResource"
|
||||||
WalletService_GrantResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResourceGroup"
|
WalletService_GrantResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GrantResourceGroup"
|
||||||
|
WalletService_RevokeResourceGrant_FullMethodName = "/hyapp.wallet.v1.WalletService/RevokeResourceGrant"
|
||||||
WalletService_ListUserResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListUserResources"
|
WalletService_ListUserResources_FullMethodName = "/hyapp.wallet.v1.WalletService/ListUserResources"
|
||||||
WalletService_EquipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/EquipUserResource"
|
WalletService_EquipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/EquipUserResource"
|
||||||
WalletService_UnequipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/UnequipUserResource"
|
WalletService_UnequipUserResource_FullMethodName = "/hyapp.wallet.v1.WalletService/UnequipUserResource"
|
||||||
@ -325,6 +326,7 @@ type WalletServiceClient interface {
|
|||||||
UpsertGiftTypeConfig(ctx context.Context, in *UpsertGiftTypeConfigRequest, opts ...grpc.CallOption) (*GiftTypeConfigResponse, error)
|
UpsertGiftTypeConfig(ctx context.Context, in *UpsertGiftTypeConfigRequest, opts ...grpc.CallOption) (*GiftTypeConfigResponse, error)
|
||||||
GrantResource(ctx context.Context, in *GrantResourceRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error)
|
GrantResource(ctx context.Context, in *GrantResourceRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error)
|
||||||
GrantResourceGroup(ctx context.Context, in *GrantResourceGroupRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error)
|
GrantResourceGroup(ctx context.Context, in *GrantResourceGroupRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error)
|
||||||
|
RevokeResourceGrant(ctx context.Context, in *RevokeResourceGrantRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error)
|
||||||
ListUserResources(ctx context.Context, in *ListUserResourcesRequest, opts ...grpc.CallOption) (*ListUserResourcesResponse, error)
|
ListUserResources(ctx context.Context, in *ListUserResourcesRequest, opts ...grpc.CallOption) (*ListUserResourcesResponse, error)
|
||||||
EquipUserResource(ctx context.Context, in *EquipUserResourceRequest, opts ...grpc.CallOption) (*EquipUserResourceResponse, error)
|
EquipUserResource(ctx context.Context, in *EquipUserResourceRequest, opts ...grpc.CallOption) (*EquipUserResourceResponse, error)
|
||||||
UnequipUserResource(ctx context.Context, in *UnequipUserResourceRequest, opts ...grpc.CallOption) (*UnequipUserResourceResponse, error)
|
UnequipUserResource(ctx context.Context, in *UnequipUserResourceRequest, opts ...grpc.CallOption) (*UnequipUserResourceResponse, error)
|
||||||
@ -699,6 +701,16 @@ func (c *walletServiceClient) GrantResourceGroup(ctx context.Context, in *GrantR
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *walletServiceClient) RevokeResourceGrant(ctx context.Context, in *RevokeResourceGrantRequest, opts ...grpc.CallOption) (*ResourceGrantResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(ResourceGrantResponse)
|
||||||
|
err := c.cc.Invoke(ctx, WalletService_RevokeResourceGrant_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *walletServiceClient) ListUserResources(ctx context.Context, in *ListUserResourcesRequest, opts ...grpc.CallOption) (*ListUserResourcesResponse, error) {
|
func (c *walletServiceClient) ListUserResources(ctx context.Context, in *ListUserResourcesRequest, opts ...grpc.CallOption) (*ListUserResourcesResponse, error) {
|
||||||
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
out := new(ListUserResourcesResponse)
|
out := new(ListUserResourcesResponse)
|
||||||
@ -1276,6 +1288,7 @@ type WalletServiceServer interface {
|
|||||||
UpsertGiftTypeConfig(context.Context, *UpsertGiftTypeConfigRequest) (*GiftTypeConfigResponse, error)
|
UpsertGiftTypeConfig(context.Context, *UpsertGiftTypeConfigRequest) (*GiftTypeConfigResponse, error)
|
||||||
GrantResource(context.Context, *GrantResourceRequest) (*ResourceGrantResponse, error)
|
GrantResource(context.Context, *GrantResourceRequest) (*ResourceGrantResponse, error)
|
||||||
GrantResourceGroup(context.Context, *GrantResourceGroupRequest) (*ResourceGrantResponse, error)
|
GrantResourceGroup(context.Context, *GrantResourceGroupRequest) (*ResourceGrantResponse, error)
|
||||||
|
RevokeResourceGrant(context.Context, *RevokeResourceGrantRequest) (*ResourceGrantResponse, error)
|
||||||
ListUserResources(context.Context, *ListUserResourcesRequest) (*ListUserResourcesResponse, error)
|
ListUserResources(context.Context, *ListUserResourcesRequest) (*ListUserResourcesResponse, error)
|
||||||
EquipUserResource(context.Context, *EquipUserResourceRequest) (*EquipUserResourceResponse, error)
|
EquipUserResource(context.Context, *EquipUserResourceRequest) (*EquipUserResourceResponse, error)
|
||||||
UnequipUserResource(context.Context, *UnequipUserResourceRequest) (*UnequipUserResourceResponse, error)
|
UnequipUserResource(context.Context, *UnequipUserResourceRequest) (*UnequipUserResourceResponse, error)
|
||||||
@ -1433,6 +1446,9 @@ func (UnimplementedWalletServiceServer) GrantResource(context.Context, *GrantRes
|
|||||||
func (UnimplementedWalletServiceServer) GrantResourceGroup(context.Context, *GrantResourceGroupRequest) (*ResourceGrantResponse, error) {
|
func (UnimplementedWalletServiceServer) GrantResourceGroup(context.Context, *GrantResourceGroupRequest) (*ResourceGrantResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method GrantResourceGroup not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method GrantResourceGroup not implemented")
|
||||||
}
|
}
|
||||||
|
func (UnimplementedWalletServiceServer) RevokeResourceGrant(context.Context, *RevokeResourceGrantRequest) (*ResourceGrantResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method RevokeResourceGrant not implemented")
|
||||||
|
}
|
||||||
func (UnimplementedWalletServiceServer) ListUserResources(context.Context, *ListUserResourcesRequest) (*ListUserResourcesResponse, error) {
|
func (UnimplementedWalletServiceServer) ListUserResources(context.Context, *ListUserResourcesRequest) (*ListUserResourcesResponse, error) {
|
||||||
return nil, status.Errorf(codes.Unimplemented, "method ListUserResources not implemented")
|
return nil, status.Errorf(codes.Unimplemented, "method ListUserResources not implemented")
|
||||||
}
|
}
|
||||||
@ -2174,6 +2190,24 @@ func _WalletService_GrantResourceGroup_Handler(srv interface{}, ctx context.Cont
|
|||||||
return interceptor(ctx, in, info, handler)
|
return interceptor(ctx, in, info, handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func _WalletService_RevokeResourceGrant_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(RevokeResourceGrantRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(WalletServiceServer).RevokeResourceGrant(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: WalletService_RevokeResourceGrant_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(WalletServiceServer).RevokeResourceGrant(ctx, req.(*RevokeResourceGrantRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
func _WalletService_ListUserResources_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
func _WalletService_ListUserResources_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
in := new(ListUserResourcesRequest)
|
in := new(ListUserResourcesRequest)
|
||||||
if err := dec(in); err != nil {
|
if err := dec(in); err != nil {
|
||||||
@ -3277,6 +3311,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
|
|||||||
MethodName: "GrantResourceGroup",
|
MethodName: "GrantResourceGroup",
|
||||||
Handler: _WalletService_GrantResourceGroup_Handler,
|
Handler: _WalletService_GrantResourceGroup_Handler,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
MethodName: "RevokeResourceGrant",
|
||||||
|
Handler: _WalletService_RevokeResourceGrant_Handler,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
MethodName: "ListUserResources",
|
MethodName: "ListUserResources",
|
||||||
Handler: _WalletService_ListUserResources_Handler,
|
Handler: _WalletService_ListUserResources_Handler,
|
||||||
|
|||||||
@ -288,6 +288,7 @@
|
|||||||
| `resource-group:update` | `button` | 编辑资源组、替换组成员、启用、禁用 |
|
| `resource-group:update` | `button` | 编辑资源组、替换组成员、启用、禁用 |
|
||||||
| `resource-grant:view` | `menu` | 资源赠送记录 |
|
| `resource-grant:view` | `menu` | 资源赠送记录 |
|
||||||
| `resource-grant:create` | `button` | 给用户赠送资源或资源组 |
|
| `resource-grant:create` | `button` | 给用户赠送资源或资源组 |
|
||||||
|
| `resource-grant:revoke` | `button` | 撤销成功的资源组赠送 |
|
||||||
| `gift:view` | `menu` | 礼物列表 |
|
| `gift:view` | `menu` | 礼物列表 |
|
||||||
| `gift:create` | `button` | 从资源库选择资源新增礼物 |
|
| `gift:create` | `button` | 从资源库选择资源新增礼物 |
|
||||||
| `gift:update` | `button` | 编辑礼物、资源绑定和价格 |
|
| `gift:update` | `button` | 编辑礼物、资源绑定和价格 |
|
||||||
|
|||||||
@ -29,6 +29,7 @@ type Client interface {
|
|||||||
UpsertGiftTypeConfig(ctx context.Context, req *walletv1.UpsertGiftTypeConfigRequest) (*walletv1.GiftTypeConfigResponse, error)
|
UpsertGiftTypeConfig(ctx context.Context, req *walletv1.UpsertGiftTypeConfigRequest) (*walletv1.GiftTypeConfigResponse, error)
|
||||||
GrantResource(ctx context.Context, req *walletv1.GrantResourceRequest) (*walletv1.ResourceGrantResponse, error)
|
GrantResource(ctx context.Context, req *walletv1.GrantResourceRequest) (*walletv1.ResourceGrantResponse, error)
|
||||||
GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error)
|
GrantResourceGroup(ctx context.Context, req *walletv1.GrantResourceGroupRequest) (*walletv1.ResourceGrantResponse, error)
|
||||||
|
RevokeResourceGrant(ctx context.Context, req *walletv1.RevokeResourceGrantRequest) (*walletv1.ResourceGrantResponse, error)
|
||||||
EquipUserResource(ctx context.Context, req *walletv1.EquipUserResourceRequest) (*walletv1.EquipUserResourceResponse, error)
|
EquipUserResource(ctx context.Context, req *walletv1.EquipUserResourceRequest) (*walletv1.EquipUserResourceResponse, error)
|
||||||
ListResourceGrants(ctx context.Context, req *walletv1.ListResourceGrantsRequest) (*walletv1.ListResourceGrantsResponse, error)
|
ListResourceGrants(ctx context.Context, req *walletv1.ListResourceGrantsRequest) (*walletv1.ListResourceGrantsResponse, error)
|
||||||
ListResourceShopItems(ctx context.Context, req *walletv1.ListResourceShopItemsRequest) (*walletv1.ListResourceShopItemsResponse, error)
|
ListResourceShopItems(ctx context.Context, req *walletv1.ListResourceShopItemsRequest) (*walletv1.ListResourceShopItemsResponse, error)
|
||||||
@ -139,6 +140,10 @@ func (c *GRPCClient) GrantResourceGroup(ctx context.Context, req *walletv1.Grant
|
|||||||
return c.client.GrantResourceGroup(ctx, req)
|
return c.client.GrantResourceGroup(ctx, req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *GRPCClient) RevokeResourceGrant(ctx context.Context, req *walletv1.RevokeResourceGrantRequest) (*walletv1.ResourceGrantResponse, error) {
|
||||||
|
return c.client.RevokeResourceGrant(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *GRPCClient) EquipUserResource(ctx context.Context, req *walletv1.EquipUserResourceRequest) (*walletv1.EquipUserResourceResponse, error) {
|
func (c *GRPCClient) EquipUserResource(ctx context.Context, req *walletv1.EquipUserResourceRequest) (*walletv1.EquipUserResourceResponse, error) {
|
||||||
return c.client.EquipUserResource(ctx, req)
|
return c.client.EquipUserResource(ctx, req)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -139,6 +139,9 @@ type grantDTO struct {
|
|||||||
Reason string `json:"reason"`
|
Reason string `json:"reason"`
|
||||||
OperatorUserID int64 `json:"operatorUserId"`
|
OperatorUserID int64 `json:"operatorUserId"`
|
||||||
Operator *operatorDTO `json:"operator,omitempty"`
|
Operator *operatorDTO `json:"operator,omitempty"`
|
||||||
|
RevokedAtMS int64 `json:"revokedAtMs"`
|
||||||
|
RevokedByUserID int64 `json:"revokedByUserId,string"`
|
||||||
|
RevokeReason string `json:"revokeReason"`
|
||||||
Items []grantItemDTO `json:"items"`
|
Items []grantItemDTO `json:"items"`
|
||||||
CreatedAtMS int64 `json:"createdAtMs"`
|
CreatedAtMS int64 `json:"createdAtMs"`
|
||||||
UpdatedAtMS int64 `json:"updatedAtMs"`
|
UpdatedAtMS int64 `json:"updatedAtMs"`
|
||||||
@ -436,6 +439,9 @@ func grantFromProto(grant *walletv1.ResourceGrant) grantDTO {
|
|||||||
Status: grant.GetStatus(),
|
Status: grant.GetStatus(),
|
||||||
Reason: grant.GetReason(),
|
Reason: grant.GetReason(),
|
||||||
OperatorUserID: grant.GetOperatorUserId(),
|
OperatorUserID: grant.GetOperatorUserId(),
|
||||||
|
RevokedAtMS: grant.GetRevokedAtMs(),
|
||||||
|
RevokedByUserID: grant.GetRevokedByUserId(),
|
||||||
|
RevokeReason: grant.GetRevokeReason(),
|
||||||
Items: items,
|
Items: items,
|
||||||
CreatedAtMS: grant.GetCreatedAtMs(),
|
CreatedAtMS: grant.GetCreatedAtMs(),
|
||||||
UpdatedAtMS: grant.GetUpdatedAtMs(),
|
UpdatedAtMS: grant.GetUpdatedAtMs(),
|
||||||
|
|||||||
@ -601,6 +601,28 @@ func (h *Handler) GrantResourceGroup(c *gin.Context) {
|
|||||||
response.Created(c, grant)
|
response.Created(c, grant)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) RevokeResourceGrant(c *gin.Context) {
|
||||||
|
grantID := strings.TrimSpace(c.Param("grant_id"))
|
||||||
|
if grantID == "" {
|
||||||
|
response.BadRequest(c, "资源赠送记录不存在")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
resp, err := h.wallet.RevokeResourceGrant(c.Request.Context(), &walletv1.RevokeResourceGrantRequest{
|
||||||
|
RequestId: middleware.CurrentRequestID(c),
|
||||||
|
AppCode: appctx.FromContext(c.Request.Context()),
|
||||||
|
GrantId: grantID,
|
||||||
|
Reason: "admin revoke resource grant",
|
||||||
|
OperatorUserId: actorID(c),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
response.BadRequest(c, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
grant := grantFromProto(resp.GetGrant())
|
||||||
|
h.auditLog(c, "revoke-resource-grant", "resource_grants", grant.GrantID, "success", fmt.Sprintf("target_user_id=%d", grant.TargetUserID))
|
||||||
|
response.OK(c, grant)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) LookupResourceGrantTarget(c *gin.Context) {
|
func (h *Handler) LookupResourceGrantTarget(c *gin.Context) {
|
||||||
keyword := strings.TrimSpace(firstQuery(c, "user_id", "userId", "keyword"))
|
keyword := strings.TrimSpace(firstQuery(c, "user_id", "userId", "keyword"))
|
||||||
if keyword == "" {
|
if keyword == "" {
|
||||||
|
|||||||
@ -43,6 +43,7 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
|
|||||||
|
|
||||||
protected.POST("/admin/resource-grants/resource", middleware.RequirePermission("resource-grant:create"), h.GrantResource)
|
protected.POST("/admin/resource-grants/resource", middleware.RequirePermission("resource-grant:create"), h.GrantResource)
|
||||||
protected.POST("/admin/resource-grants/group", middleware.RequirePermission("resource-grant:create"), h.GrantResourceGroup)
|
protected.POST("/admin/resource-grants/group", middleware.RequirePermission("resource-grant:create"), h.GrantResourceGroup)
|
||||||
|
protected.POST("/admin/resource-grants/:grant_id/revoke", middleware.RequirePermission("resource-grant:revoke"), h.RevokeResourceGrant)
|
||||||
protected.GET("/admin/resource-grants/target", middleware.RequirePermission("resource-grant:create"), h.LookupResourceGrantTarget)
|
protected.GET("/admin/resource-grants/target", middleware.RequirePermission("resource-grant:create"), h.LookupResourceGrantTarget)
|
||||||
protected.GET("/admin/resource-grants", middleware.RequirePermission("resource-grant:view"), h.ListResourceGrants)
|
protected.GET("/admin/resource-grants", middleware.RequirePermission("resource-grant:view"), h.ListResourceGrants)
|
||||||
|
|
||||||
|
|||||||
@ -57,6 +57,7 @@ var defaultPermissions = []model.Permission{
|
|||||||
{Name: "资源组更新", Code: "resource-group:update", Kind: "button"},
|
{Name: "资源组更新", Code: "resource-group:update", Kind: "button"},
|
||||||
{Name: "资源赠送查看", Code: "resource-grant:view", Kind: "menu"},
|
{Name: "资源赠送查看", Code: "resource-grant:view", Kind: "menu"},
|
||||||
{Name: "资源赠送创建", Code: "resource-grant:create", Kind: "button"},
|
{Name: "资源赠送创建", Code: "resource-grant:create", Kind: "button"},
|
||||||
|
{Name: "资源赠送撤销", Code: "resource-grant:revoke", Kind: "button"},
|
||||||
{Name: "礼物查看", Code: "gift:view", Kind: "menu"},
|
{Name: "礼物查看", Code: "gift:view", Kind: "menu"},
|
||||||
{Name: "礼物创建", Code: "gift:create", Kind: "button"},
|
{Name: "礼物创建", Code: "gift:create", Kind: "button"},
|
||||||
{Name: "礼物更新", Code: "gift:update", Kind: "button"},
|
{Name: "礼物更新", Code: "gift:update", Kind: "button"},
|
||||||
@ -534,7 +535,7 @@ func defaultRolePermissionCodes(code string) []string {
|
|||||||
"resource:view", "resource:create", "resource:update",
|
"resource:view", "resource:create", "resource:update",
|
||||||
"resource-shop:view", "resource-shop:update",
|
"resource-shop:view", "resource-shop:update",
|
||||||
"resource-group:view", "resource-group:create", "resource-group:update",
|
"resource-group:view", "resource-group:create", "resource-group:update",
|
||||||
"resource-grant:view", "resource-grant:create",
|
"resource-grant:view", "resource-grant:create", "resource-grant:revoke",
|
||||||
"gift:view", "gift:create", "gift:update", "gift:status", "gift:delete",
|
"gift:view", "gift:create", "gift:update", "gift:status", "gift:delete",
|
||||||
"emoji-pack:view", "emoji-pack:create",
|
"emoji-pack:view", "emoji-pack:create",
|
||||||
"country:view", "country:create", "country:update", "country:status",
|
"country:view", "country:create", "country:update", "country:status",
|
||||||
@ -637,7 +638,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
|
|||||||
"resource:view", "resource:create", "resource:update",
|
"resource:view", "resource:create", "resource:update",
|
||||||
"resource-shop:view", "resource-shop:update",
|
"resource-shop:view", "resource-shop:update",
|
||||||
"resource-group:view", "resource-group:create", "resource-group:update",
|
"resource-group:view", "resource-group:create", "resource-group:update",
|
||||||
"resource-grant:view", "resource-grant:create",
|
"resource-grant:view", "resource-grant:create", "resource-grant:revoke",
|
||||||
"gift:view", "gift:create", "gift:update", "gift:status", "gift:delete",
|
"gift:view", "gift:create", "gift:update", "gift:status", "gift:delete",
|
||||||
"emoji-pack:view", "emoji-pack:create",
|
"emoji-pack:view", "emoji-pack:create",
|
||||||
"upload:create",
|
"upload:create",
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 资源赠送撤销会触发钱包扣回和权益失效,必须独立成按钮权限,不能复用创建权限。
|
||||||
|
SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
|
||||||
|
|
||||||
|
INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES
|
||||||
|
('资源赠送撤销', 'resource-grant:revoke', '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 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 = 'resource-grant:revoke';
|
||||||
@ -1140,6 +1140,9 @@ CREATE TABLE IF NOT EXISTS resource_grants (
|
|||||||
group_snapshot_json JSON NULL COMMENT '资源分组快照 JSON',
|
group_snapshot_json JSON NULL COMMENT '资源分组快照 JSON',
|
||||||
reason VARCHAR(256) NOT NULL COMMENT '原因',
|
reason VARCHAR(256) NOT NULL COMMENT '原因',
|
||||||
operator_user_id BIGINT NOT NULL COMMENT '操作人用户 ID',
|
operator_user_id BIGINT NOT NULL COMMENT '操作人用户 ID',
|
||||||
|
revoked_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '撤销时间,UTC epoch ms',
|
||||||
|
revoked_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '撤销操作人用户 ID',
|
||||||
|
revoke_reason VARCHAR(256) NOT NULL DEFAULT '' COMMENT '撤销原因',
|
||||||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||||||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||||||
PRIMARY KEY (app_code, grant_id),
|
PRIMARY KEY (app_code, grant_id),
|
||||||
|
|||||||
@ -32,6 +32,7 @@ const (
|
|||||||
GrantSourceResourceShop = "resource_shop"
|
GrantSourceResourceShop = "resource_shop"
|
||||||
GrantSourceGameRobotInit = "game_robot_init"
|
GrantSourceGameRobotInit = "game_robot_init"
|
||||||
GrantStatusDone = "succeeded"
|
GrantStatusDone = "succeeded"
|
||||||
|
GrantStatusRevoked = "revoked"
|
||||||
ResultWalletCredit = "wallet_credit"
|
ResultWalletCredit = "wallet_credit"
|
||||||
ResultEntitlement = "entitlement"
|
ResultEntitlement = "entitlement"
|
||||||
|
|
||||||
|
|||||||
@ -24,6 +24,9 @@ type ResourceGrant struct {
|
|||||||
Status string
|
Status string
|
||||||
Reason string
|
Reason string
|
||||||
OperatorUserID int64
|
OperatorUserID int64
|
||||||
|
RevokedAtMS int64
|
||||||
|
RevokedByUserID int64
|
||||||
|
RevokeReason string
|
||||||
Items []ResourceGrantItem
|
Items []ResourceGrantItem
|
||||||
CreatedAtMS int64
|
CreatedAtMS int64
|
||||||
UpdatedAtMS int64
|
UpdatedAtMS int64
|
||||||
@ -51,6 +54,14 @@ type GrantResourceGroupCommand struct {
|
|||||||
GrantSource string
|
GrantSource string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RevokeResourceGrantCommand struct {
|
||||||
|
AppCode string
|
||||||
|
RequestID string
|
||||||
|
GrantID string
|
||||||
|
Reason string
|
||||||
|
OperatorUserID int64
|
||||||
|
}
|
||||||
|
|
||||||
type ListResourceGrantsQuery struct {
|
type ListResourceGrantsQuery struct {
|
||||||
AppCode string
|
AppCode string
|
||||||
TargetUserID int64
|
TargetUserID int64
|
||||||
|
|||||||
@ -165,6 +165,7 @@ type ResourceCatalogStore interface {
|
|||||||
type ResourceGrantStore interface {
|
type ResourceGrantStore interface {
|
||||||
GrantResource(ctx context.Context, command resourcedomain.GrantResourceCommand) (resourcedomain.ResourceGrant, error)
|
GrantResource(ctx context.Context, command resourcedomain.GrantResourceCommand) (resourcedomain.ResourceGrant, error)
|
||||||
GrantResourceGroup(ctx context.Context, command resourcedomain.GrantResourceGroupCommand) (resourcedomain.ResourceGrant, error)
|
GrantResourceGroup(ctx context.Context, command resourcedomain.GrantResourceGroupCommand) (resourcedomain.ResourceGrant, error)
|
||||||
|
RevokeResourceGrant(ctx context.Context, command resourcedomain.RevokeResourceGrantCommand) (resourcedomain.ResourceGrant, error)
|
||||||
ListResourceGrants(ctx context.Context, query resourcedomain.ListResourceGrantsQuery) ([]resourcedomain.ResourceGrant, int64, error)
|
ListResourceGrants(ctx context.Context, query resourcedomain.ListResourceGrantsQuery) ([]resourcedomain.ResourceGrant, int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -190,6 +190,15 @@ func (s *Service) GrantResourceGroup(ctx context.Context, command resourcedomain
|
|||||||
return s.repository.GrantResourceGroup(ctx, command)
|
return s.repository.GrantResourceGroup(ctx, command)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) RevokeResourceGrant(ctx context.Context, command resourcedomain.RevokeResourceGrantCommand) (resourcedomain.ResourceGrant, error) {
|
||||||
|
if s.repository == nil {
|
||||||
|
return resourcedomain.ResourceGrant{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||||
|
}
|
||||||
|
command.AppCode = appcode.Normalize(command.AppCode)
|
||||||
|
ctx = appcode.WithContext(ctx, command.AppCode)
|
||||||
|
return s.repository.RevokeResourceGrant(ctx, command)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) ListUserResources(ctx context.Context, query resourcedomain.ListUserResourcesQuery) ([]resourcedomain.UserResourceEntitlement, error) {
|
func (s *Service) ListUserResources(ctx context.Context, query resourcedomain.ListUserResourcesQuery) ([]resourcedomain.UserResourceEntitlement, error) {
|
||||||
if s.repository == nil {
|
if s.repository == nil {
|
||||||
return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
|
||||||
|
|||||||
@ -3944,6 +3944,254 @@ func TestGrantResourceGroupExpandsWalletAssetAndEntitlement(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestRevokeResourceGroupGrantReversesWalletCreditAndEntitlement 锁定撤销只按原发放明细反向处理,不受资源组后续配置变化影响。
|
||||||
|
func TestRevokeResourceGroupGrantReversesWalletCreditAndEntitlement(t *testing.T) {
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
svc := walletservice.New(repository)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
badgeResource, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
|
||||||
|
ResourceCode: "badge_revoke_vip",
|
||||||
|
ResourceType: resourcedomain.TypeBadge,
|
||||||
|
Name: "Revoke VIP Badge",
|
||||||
|
Status: resourcedomain.StatusActive,
|
||||||
|
Grantable: true,
|
||||||
|
GrantStrategy: resourcedomain.GrantStrategySetActiveFlag,
|
||||||
|
UsageScopes: []string{"profile"},
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create revoke badge resource failed: %v", err)
|
||||||
|
}
|
||||||
|
group, err := svc.CreateResourceGroup(ctx, resourcedomain.ResourceGroupCommand{
|
||||||
|
GroupCode: "revoke_pack",
|
||||||
|
Name: "Revoke Pack",
|
||||||
|
Status: resourcedomain.StatusActive,
|
||||||
|
Items: []resourcedomain.ResourceGroupItemInput{
|
||||||
|
{ItemType: resourcedomain.GroupItemTypeWalletAsset, WalletAssetType: ledger.AssetCoin, WalletAssetAmount: 2000, SortOrder: 1},
|
||||||
|
{ResourceID: badgeResource.ResourceID, Quantity: 1, SortOrder: 2},
|
||||||
|
},
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create revoke resource group failed: %v", err)
|
||||||
|
}
|
||||||
|
grant, err := svc.GrantResourceGroup(ctx, resourcedomain.GrantResourceGroupCommand{
|
||||||
|
CommandID: "cmd-grant-revoke-pack",
|
||||||
|
TargetUserID: 42101,
|
||||||
|
GroupID: group.GroupID,
|
||||||
|
Reason: "test revoke group",
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
GrantSource: resourcedomain.GrantSourceAdmin,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GrantResourceGroup for revoke failed: %v", err)
|
||||||
|
}
|
||||||
|
entitlementID := entitlementIDFromGrant(t, grant)
|
||||||
|
|
||||||
|
revoked, err := svc.RevokeResourceGrant(ctx, resourcedomain.RevokeResourceGrantCommand{
|
||||||
|
RequestID: "cmd-revoke-pack",
|
||||||
|
GrantID: grant.GrantID,
|
||||||
|
Reason: "test revoke",
|
||||||
|
OperatorUserID: 90002,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("RevokeResourceGrant failed: %v", err)
|
||||||
|
}
|
||||||
|
if revoked.Status != resourcedomain.GrantStatusRevoked || revoked.RevokedAtMS == 0 || revoked.RevokedByUserID != 90002 || revoked.RevokeReason != "test revoke" {
|
||||||
|
t.Fatalf("revoked grant metadata mismatch: %+v", revoked)
|
||||||
|
}
|
||||||
|
balances, err := svc.GetBalances(ctx, 42101, []string{ledger.AssetCoin})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetBalances after revoke failed: %v", err)
|
||||||
|
}
|
||||||
|
if balanceAmount(balances, ledger.AssetCoin) != 0 {
|
||||||
|
t.Fatalf("revoked coin balance mismatch: %+v", balances)
|
||||||
|
}
|
||||||
|
if got := repository.CountRows("wallet_entries", "user_id = ? AND asset_type = ? AND available_delta = ?", int64(42101), ledger.AssetCoin, int64(-2000)); got != 1 {
|
||||||
|
t.Fatalf("revoke should write one reverse coin entry, got %d", got)
|
||||||
|
}
|
||||||
|
if got := repository.CountRows("wallet_transactions", "external_ref = ? AND biz_type = ?", grant.GrantID, "resource_grant_revoke"); got != 1 {
|
||||||
|
t.Fatalf("revoke should write one reverse wallet transaction, got %d", got)
|
||||||
|
}
|
||||||
|
if got := repository.CountRows("user_resource_entitlements", "entitlement_id = ? AND status = ? AND remaining_quantity = 0", entitlementID, resourcedomain.GrantStatusRevoked); got != 1 {
|
||||||
|
t.Fatalf("revoke should mark entitlement unavailable, got %d", got)
|
||||||
|
}
|
||||||
|
if got := repository.CountRows("user_resource_equipment", "user_id = ? AND entitlement_id = ?", int64(42101), entitlementID); got != 0 {
|
||||||
|
t.Fatalf("revoke should remove equipment row, got %d", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
again, err := svc.RevokeResourceGrant(ctx, resourcedomain.RevokeResourceGrantCommand{
|
||||||
|
RequestID: "cmd-revoke-pack-again",
|
||||||
|
GrantID: grant.GrantID,
|
||||||
|
Reason: "test revoke again",
|
||||||
|
OperatorUserID: 90003,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("RevokeResourceGrant retry failed: %v", err)
|
||||||
|
}
|
||||||
|
if again.Status != resourcedomain.GrantStatusRevoked || again.RevokedByUserID != 90002 {
|
||||||
|
t.Fatalf("idempotent revoke should keep first metadata: %+v", again)
|
||||||
|
}
|
||||||
|
if got := repository.CountRows("wallet_entries", "user_id = ? AND asset_type = ? AND available_delta = ?", int64(42101), ledger.AssetCoin, int64(-2000)); got != 1 {
|
||||||
|
t.Fatalf("idempotent revoke must not double debit, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRevokeResourceGroupGrantFailsWhenBalanceInsufficient 验证余额不足时整个撤销事务回滚,权益和发放记录仍保持成功状态。
|
||||||
|
func TestRevokeResourceGroupGrantFailsWhenBalanceInsufficient(t *testing.T) {
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
svc := walletservice.New(repository)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
badgeResource, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
|
||||||
|
ResourceCode: "badge_revoke_insufficient",
|
||||||
|
ResourceType: resourcedomain.TypeBadge,
|
||||||
|
Name: "Revoke Insufficient Badge",
|
||||||
|
Status: resourcedomain.StatusActive,
|
||||||
|
Grantable: true,
|
||||||
|
GrantStrategy: resourcedomain.GrantStrategySetActiveFlag,
|
||||||
|
UsageScopes: []string{"profile"},
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create insufficient badge resource failed: %v", err)
|
||||||
|
}
|
||||||
|
group, err := svc.CreateResourceGroup(ctx, resourcedomain.ResourceGroupCommand{
|
||||||
|
GroupCode: "revoke_insufficient_pack",
|
||||||
|
Name: "Revoke Insufficient Pack",
|
||||||
|
Status: resourcedomain.StatusActive,
|
||||||
|
Items: []resourcedomain.ResourceGroupItemInput{
|
||||||
|
{ItemType: resourcedomain.GroupItemTypeWalletAsset, WalletAssetType: ledger.AssetCoin, WalletAssetAmount: 2000, SortOrder: 1},
|
||||||
|
{ResourceID: badgeResource.ResourceID, Quantity: 1, SortOrder: 2},
|
||||||
|
},
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create insufficient resource group failed: %v", err)
|
||||||
|
}
|
||||||
|
grant, err := svc.GrantResourceGroup(ctx, resourcedomain.GrantResourceGroupCommand{
|
||||||
|
CommandID: "cmd-grant-revoke-insufficient",
|
||||||
|
TargetUserID: 42102,
|
||||||
|
GroupID: group.GroupID,
|
||||||
|
Reason: "test revoke insufficient",
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
GrantSource: resourcedomain.GrantSourceAdmin,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GrantResourceGroup insufficient setup failed: %v", err)
|
||||||
|
}
|
||||||
|
entitlementID := entitlementIDFromGrant(t, grant)
|
||||||
|
repository.SetBalance(42102, 100)
|
||||||
|
|
||||||
|
_, err = svc.RevokeResourceGrant(ctx, resourcedomain.RevokeResourceGrantCommand{
|
||||||
|
RequestID: "cmd-revoke-insufficient",
|
||||||
|
GrantID: grant.GrantID,
|
||||||
|
Reason: "test insufficient",
|
||||||
|
OperatorUserID: 90002,
|
||||||
|
})
|
||||||
|
if !xerr.IsCode(err, xerr.InsufficientBalance) {
|
||||||
|
t.Fatalf("revoke should fail with insufficient balance, got %v", err)
|
||||||
|
}
|
||||||
|
if got := repository.CountRows("resource_grants", "grant_id = ? AND status = ?", grant.GrantID, resourcedomain.GrantStatusDone); got != 1 {
|
||||||
|
t.Fatalf("failed revoke must keep grant succeeded, got %d", got)
|
||||||
|
}
|
||||||
|
if got := repository.CountRows("user_resource_entitlements", "entitlement_id = ? AND status = 'active'", entitlementID); got != 1 {
|
||||||
|
t.Fatalf("failed revoke must keep entitlement active, got %d", got)
|
||||||
|
}
|
||||||
|
if got := repository.CountRows("user_resource_equipment", "user_id = ? AND entitlement_id = ?", int64(42102), entitlementID); got != 1 {
|
||||||
|
t.Fatalf("failed revoke must keep equipment row, got %d", got)
|
||||||
|
}
|
||||||
|
if got := repository.CountRows("wallet_transactions", "external_ref = ? AND biz_type = ?", grant.GrantID, "resource_grant_revoke"); got != 0 {
|
||||||
|
t.Fatalf("failed revoke must not write reverse transaction, got %d", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRevokeResourceGrantRejectsUnsupportedRecords 固定撤销范围:只允许成功的资源组发放,单资源、异常状态和不存在记录都不能误处理。
|
||||||
|
func TestRevokeResourceGrantRejectsUnsupportedRecords(t *testing.T) {
|
||||||
|
repository := mysqltest.NewRepository(t)
|
||||||
|
svc := walletservice.New(repository)
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
coinResource, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
|
||||||
|
ResourceCode: "coin_single_revoke_reject",
|
||||||
|
ResourceType: resourcedomain.TypeCoin,
|
||||||
|
Name: "Coin Single Revoke Reject",
|
||||||
|
Status: resourcedomain.StatusActive,
|
||||||
|
Grantable: true,
|
||||||
|
GrantStrategy: resourcedomain.GrantStrategyWalletCredit,
|
||||||
|
WalletAssetType: ledger.AssetCoin,
|
||||||
|
WalletAssetAmount: 100,
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create single coin resource failed: %v", err)
|
||||||
|
}
|
||||||
|
singleGrant, err := svc.GrantResource(ctx, resourcedomain.GrantResourceCommand{
|
||||||
|
CommandID: "cmd-single-revoke-reject",
|
||||||
|
TargetUserID: 42103,
|
||||||
|
ResourceID: coinResource.ResourceID,
|
||||||
|
Quantity: 1,
|
||||||
|
Reason: "single reject",
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
GrantSource: resourcedomain.GrantSourceAdmin,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("single GrantResource setup failed: %v", err)
|
||||||
|
}
|
||||||
|
_, err = svc.RevokeResourceGrant(ctx, resourcedomain.RevokeResourceGrantCommand{
|
||||||
|
RequestID: "cmd-revoke-single-reject",
|
||||||
|
GrantID: singleGrant.GrantID,
|
||||||
|
Reason: "single reject",
|
||||||
|
OperatorUserID: 90002,
|
||||||
|
})
|
||||||
|
if !xerr.IsCode(err, xerr.InvalidArgument) {
|
||||||
|
t.Fatalf("single resource revoke should be invalid, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
group, err := svc.CreateResourceGroup(ctx, resourcedomain.ResourceGroupCommand{
|
||||||
|
GroupCode: "failed_revoke_reject_pack",
|
||||||
|
Name: "Failed Revoke Reject Pack",
|
||||||
|
Status: resourcedomain.StatusActive,
|
||||||
|
Items: []resourcedomain.ResourceGroupItemInput{{ItemType: resourcedomain.GroupItemTypeWalletAsset, WalletAssetType: ledger.AssetCoin, WalletAssetAmount: 10}},
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create failed-status resource group failed: %v", err)
|
||||||
|
}
|
||||||
|
groupGrant, err := svc.GrantResourceGroup(ctx, resourcedomain.GrantResourceGroupCommand{
|
||||||
|
CommandID: "cmd-failed-revoke-reject",
|
||||||
|
TargetUserID: 42104,
|
||||||
|
GroupID: group.GroupID,
|
||||||
|
Reason: "failed reject",
|
||||||
|
OperatorUserID: 90001,
|
||||||
|
GrantSource: resourcedomain.GrantSourceAdmin,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("group GrantResourceGroup setup failed: %v", err)
|
||||||
|
}
|
||||||
|
repository.SetResourceGrantStatus(groupGrant.GrantID, "failed")
|
||||||
|
_, err = svc.RevokeResourceGrant(ctx, resourcedomain.RevokeResourceGrantCommand{
|
||||||
|
RequestID: "cmd-revoke-failed-reject",
|
||||||
|
GrantID: groupGrant.GrantID,
|
||||||
|
Reason: "failed reject",
|
||||||
|
OperatorUserID: 90002,
|
||||||
|
})
|
||||||
|
if !xerr.IsCode(err, xerr.Conflict) {
|
||||||
|
t.Fatalf("failed grant revoke should conflict, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = svc.RevokeResourceGrant(ctx, resourcedomain.RevokeResourceGrantCommand{
|
||||||
|
RequestID: "cmd-revoke-missing",
|
||||||
|
GrantID: "rgr_missing_revoke",
|
||||||
|
Reason: "missing reject",
|
||||||
|
OperatorUserID: 90002,
|
||||||
|
})
|
||||||
|
if !xerr.IsCode(err, xerr.NotFound) {
|
||||||
|
t.Fatalf("missing grant revoke should be not found, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestGrantMicSeatAnimationAutoEquips 验证麦位声波资源在发放事务内默认佩戴。
|
// TestGrantMicSeatAnimationAutoEquips 验证麦位声波资源在发放事务内默认佩戴。
|
||||||
func TestGrantMicSeatAnimationAutoEquips(t *testing.T) {
|
func TestGrantMicSeatAnimationAutoEquips(t *testing.T) {
|
||||||
repository := mysqltest.NewRepository(t)
|
repository := mysqltest.NewRepository(t)
|
||||||
@ -5584,6 +5832,17 @@ func balanceAmount(balances []ledger.AssetBalance, assetType string) int64 {
|
|||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func entitlementIDFromGrant(t *testing.T, grant resourcedomain.ResourceGrant) string {
|
||||||
|
t.Helper()
|
||||||
|
for _, item := range grant.Items {
|
||||||
|
if item.ResultType == resourcedomain.ResultEntitlement && item.EntitlementID != "" {
|
||||||
|
return item.EntitlementID
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.Fatalf("grant has no entitlement item: %+v", grant)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
func assertBalance(t *testing.T, svc *walletservice.Service, userID int64, assetType string, want int64) {
|
func assertBalance(t *testing.T, svc *walletservice.Service, userID int64, assetType string, want int64) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
@ -53,6 +53,10 @@ func Open(ctx context.Context, dsn string) (*Repository, error) {
|
|||||||
_ = db.Close()
|
_ = db.Close()
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := ensureResourceGrantRevokeSchema(ctx, db); err != nil {
|
||||||
|
_ = db.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &Repository{db: db}, nil
|
return &Repository{db: db}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
bizTypeResourceGrant = "resource_grant"
|
bizTypeResourceGrant = "resource_grant"
|
||||||
|
bizTypeResourceGrantRevoke = "resource_grant_revoke"
|
||||||
bizTypeResourceShopPurchase = "resource_shop_purchase"
|
bizTypeResourceShopPurchase = "resource_shop_purchase"
|
||||||
resourceOutboxAsset = "RESOURCE"
|
resourceOutboxAsset = "RESOURCE"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"hyapp/pkg/appcode"
|
"hyapp/pkg/appcode"
|
||||||
"hyapp/pkg/xerr"
|
"hyapp/pkg/xerr"
|
||||||
resourcedomain "hyapp/services/wallet-service/internal/domain/resource"
|
resourcedomain "hyapp/services/wallet-service/internal/domain/resource"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@ -136,6 +137,116 @@ func (r *Repository) GrantResourceGroup(ctx context.Context, command resourcedom
|
|||||||
return r.GetResourceGrant(ctx, grantID)
|
return r.GetResourceGrant(ctx, grantID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RevokeResourceGrant 撤销一次成功的资源组发放;钱包扣回、权益失效、装备清理和发放状态更新必须同事务提交。
|
||||||
|
func (r *Repository) RevokeResourceGrant(ctx context.Context, command resourcedomain.RevokeResourceGrantCommand) (resourcedomain.ResourceGrant, error) {
|
||||||
|
if r == nil || r.db == nil {
|
||||||
|
return resourcedomain.ResourceGrant{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
|
}
|
||||||
|
ctx = contextWithCommandApp(ctx, command.AppCode)
|
||||||
|
command.AppCode = appcode.FromContext(ctx)
|
||||||
|
command = normalizeRevokeResourceGrantCommand(command)
|
||||||
|
if err := validateRevokeResourceGrantCommand(command); err != nil {
|
||||||
|
return resourcedomain.ResourceGrant{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := r.db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return resourcedomain.ResourceGrant{}, err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
grant, err := r.lockResourceGrantTx(ctx, tx, command.GrantID)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return resourcedomain.ResourceGrant{}, xerr.New(xerr.NotFound, "resource grant not found")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return resourcedomain.ResourceGrant{}, err
|
||||||
|
}
|
||||||
|
if grant.Status == resourcedomain.GrantStatusRevoked {
|
||||||
|
grant.Items, err = r.listResourceGrantItemsTx(ctx, tx, grant.GrantID)
|
||||||
|
if err != nil {
|
||||||
|
return resourcedomain.ResourceGrant{}, err
|
||||||
|
}
|
||||||
|
return grant, nil
|
||||||
|
}
|
||||||
|
if grant.GrantSubjectType != resourcedomain.GrantSubjectGroup {
|
||||||
|
return resourcedomain.ResourceGrant{}, xerr.New(xerr.InvalidArgument, "only resource group grant can be revoked")
|
||||||
|
}
|
||||||
|
if grant.Status != resourcedomain.GrantStatusDone {
|
||||||
|
return resourcedomain.ResourceGrant{}, xerr.New(xerr.Conflict, "resource grant is not succeeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
grant.Items, err = r.listResourceGrantItemsTx(ctx, tx, grant.GrantID)
|
||||||
|
if err != nil {
|
||||||
|
return resourcedomain.ResourceGrant{}, err
|
||||||
|
}
|
||||||
|
nowMs := time.Now().UnixMilli()
|
||||||
|
outboxEvents := make([]walletOutboxEvent, 0, len(grant.Items)+1)
|
||||||
|
for _, item := range grant.Items {
|
||||||
|
switch item.ResultType {
|
||||||
|
case resourcedomain.ResultWalletCredit:
|
||||||
|
event, err := r.revokeGrantedWalletCredit(ctx, tx, grant, item, command, nowMs)
|
||||||
|
if err != nil {
|
||||||
|
return resourcedomain.ResourceGrant{}, err
|
||||||
|
}
|
||||||
|
outboxEvents = append(outboxEvents, event)
|
||||||
|
case resourcedomain.ResultEntitlement:
|
||||||
|
event, err := r.revokeGrantedEntitlement(ctx, tx, grant, item, command, nowMs)
|
||||||
|
if err != nil {
|
||||||
|
return resourcedomain.ResourceGrant{}, err
|
||||||
|
}
|
||||||
|
outboxEvents = append(outboxEvents, event)
|
||||||
|
default:
|
||||||
|
return resourcedomain.ResourceGrant{}, xerr.New(xerr.LedgerConflict, "resource grant item result type is invalid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := tx.ExecContext(ctx, `
|
||||||
|
UPDATE resource_grants
|
||||||
|
SET status = ?, revoked_at_ms = ?, revoked_by_user_id = ?, revoke_reason = ?, updated_at_ms = ?
|
||||||
|
WHERE app_code = ? AND grant_id = ? AND status = ?`,
|
||||||
|
resourcedomain.GrantStatusRevoked, nowMs, command.OperatorUserID, command.Reason, nowMs,
|
||||||
|
command.AppCode, grant.GrantID, resourcedomain.GrantStatusDone,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return resourcedomain.ResourceGrant{}, err
|
||||||
|
}
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return resourcedomain.ResourceGrant{}, err
|
||||||
|
}
|
||||||
|
if affected != 1 {
|
||||||
|
return resourcedomain.ResourceGrant{}, xerr.New(xerr.LedgerConflict, "resource grant revoke state conflict")
|
||||||
|
}
|
||||||
|
|
||||||
|
revokeCommandID := resourceGrantRevokeCommandID(command, grant.GrantID, 0)
|
||||||
|
groupID, _ := strconv.ParseInt(strings.TrimSpace(grant.GrantSubjectID), 10, 64)
|
||||||
|
outboxEvents = append(outboxEvents, resourceOutboxEvent("ResourceGrantRevoked", revokeCommandID, grant.TargetUserID, groupID, map[string]any{
|
||||||
|
"action": "revoke",
|
||||||
|
"app_code": command.AppCode,
|
||||||
|
"grant_id": grant.GrantID,
|
||||||
|
"target_user_id": grant.TargetUserID,
|
||||||
|
"grant_subject_type": grant.GrantSubjectType,
|
||||||
|
"grant_subject_id": grant.GrantSubjectID,
|
||||||
|
"revoked_by_user_id": command.OperatorUserID,
|
||||||
|
"revoke_reason": command.Reason,
|
||||||
|
"revoked_at_ms": nowMs,
|
||||||
|
"event_command": revokeCommandID,
|
||||||
|
}, nowMs))
|
||||||
|
if err := r.insertWalletOutbox(ctx, tx, outboxEvents); err != nil {
|
||||||
|
return resourcedomain.ResourceGrant{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
revoked, err := r.getResourceGrantTx(ctx, tx, grant.GrantID)
|
||||||
|
if err != nil {
|
||||||
|
return resourcedomain.ResourceGrant{}, err
|
||||||
|
}
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return resourcedomain.ResourceGrant{}, err
|
||||||
|
}
|
||||||
|
return revoked, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) ListResourceGrants(ctx context.Context, query resourcedomain.ListResourceGrantsQuery) ([]resourcedomain.ResourceGrant, int64, error) {
|
func (r *Repository) ListResourceGrants(ctx context.Context, query resourcedomain.ListResourceGrantsQuery) ([]resourcedomain.ResourceGrant, int64, error) {
|
||||||
if r == nil || r.db == nil {
|
if r == nil || r.db == nil {
|
||||||
return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
return nil, 0, xerr.New(xerr.Unavailable, "mysql repository is not configured")
|
||||||
@ -352,6 +463,128 @@ func (r *Repository) applyWalletAssetGrantItem(ctx context.Context, tx *sql.Tx,
|
|||||||
return inserted, nil
|
return inserted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) revokeGrantedWalletCredit(ctx context.Context, tx *sql.Tx, grant resourcedomain.ResourceGrant, item resourcedomain.ResourceGrantItem, command resourcedomain.RevokeResourceGrantCommand, nowMs int64) (walletOutboxEvent, error) {
|
||||||
|
if strings.TrimSpace(item.WalletTransactionID) == "" {
|
||||||
|
return walletOutboxEvent{}, xerr.New(xerr.LedgerConflict, "resource grant wallet transaction is missing")
|
||||||
|
}
|
||||||
|
// 反扣金额以原入账分录为准,避免资源组或资源定义后续变更导致撤销扣回的内容漂移。
|
||||||
|
original, err := r.lockOriginalGrantCreditEntry(ctx, tx, item.WalletTransactionID, grant.TargetUserID)
|
||||||
|
if err != nil {
|
||||||
|
return walletOutboxEvent{}, err
|
||||||
|
}
|
||||||
|
if original.AvailableDelta <= 0 || original.FrozenDelta != 0 {
|
||||||
|
return walletOutboxEvent{}, xerr.New(xerr.LedgerConflict, "resource grant wallet entry is not reversible")
|
||||||
|
}
|
||||||
|
account, err := r.lockAccount(ctx, tx, grant.TargetUserID, original.AssetType, false, nowMs)
|
||||||
|
if err != nil {
|
||||||
|
return walletOutboxEvent{}, err
|
||||||
|
}
|
||||||
|
if account.AvailableAmount < original.AvailableDelta {
|
||||||
|
return walletOutboxEvent{}, xerr.New(xerr.InsufficientBalance, "insufficient balance to revoke resource grant")
|
||||||
|
}
|
||||||
|
|
||||||
|
walletCommandID := resourceGrantRevokeCommandID(command, grant.GrantID, item.GrantItemID)
|
||||||
|
transactionID := transactionID(command.AppCode, walletCommandID)
|
||||||
|
metadata := map[string]any{
|
||||||
|
"app_code": command.AppCode,
|
||||||
|
"grant_id": grant.GrantID,
|
||||||
|
"grant_item_id": item.GrantItemID,
|
||||||
|
"target_user_id": grant.TargetUserID,
|
||||||
|
"asset_type": original.AssetType,
|
||||||
|
"wallet_amount": original.AvailableDelta,
|
||||||
|
"original_transaction_id": item.WalletTransactionID,
|
||||||
|
"revoked_by_user_id": command.OperatorUserID,
|
||||||
|
"revoke_reason": command.Reason,
|
||||||
|
"operator_reason": "resource grant revoke",
|
||||||
|
}
|
||||||
|
if err := r.insertTransaction(ctx, tx, transactionID, walletCommandID, bizTypeResourceGrantRevoke, resourceGrantWalletRevokeHash(grant.GrantID, item.GrantItemID, original.AssetType, original.AvailableDelta), grant.GrantID, metadata, nowMs); err != nil {
|
||||||
|
return walletOutboxEvent{}, err
|
||||||
|
}
|
||||||
|
if err := r.applyAccountDelta(ctx, tx, account, -original.AvailableDelta, 0, nowMs); err != nil {
|
||||||
|
return walletOutboxEvent{}, err
|
||||||
|
}
|
||||||
|
availableAfter := account.AvailableAmount - original.AvailableDelta
|
||||||
|
if err := r.insertEntry(ctx, tx, walletEntry{
|
||||||
|
TransactionID: transactionID,
|
||||||
|
UserID: grant.TargetUserID,
|
||||||
|
AssetType: original.AssetType,
|
||||||
|
AvailableDelta: -original.AvailableDelta,
|
||||||
|
FrozenDelta: 0,
|
||||||
|
AvailableAfter: availableAfter,
|
||||||
|
FrozenAfter: account.FrozenAmount,
|
||||||
|
CreatedAtMS: nowMs,
|
||||||
|
}); err != nil {
|
||||||
|
return walletOutboxEvent{}, err
|
||||||
|
}
|
||||||
|
return balanceChangedEvent(transactionID, walletCommandID, grant.TargetUserID, original.AssetType, -original.AvailableDelta, 0, availableAfter, account.FrozenAmount, account.Version+1, metadata, nowMs), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) revokeGrantedEntitlement(ctx context.Context, tx *sql.Tx, grant resourcedomain.ResourceGrant, item resourcedomain.ResourceGrantItem, command resourcedomain.RevokeResourceGrantCommand, nowMs int64) (walletOutboxEvent, error) {
|
||||||
|
if strings.TrimSpace(item.EntitlementID) == "" {
|
||||||
|
return walletOutboxEvent{}, xerr.New(xerr.LedgerConflict, "resource grant entitlement is missing")
|
||||||
|
}
|
||||||
|
// 撤销不折算有效期,直接让原发放产生的权益不可用;装备表同步删除,避免客户端继续展示已穿戴状态。
|
||||||
|
result, err := tx.ExecContext(ctx, `
|
||||||
|
UPDATE user_resource_entitlements
|
||||||
|
SET status = ?, remaining_quantity = 0, updated_at_ms = ?
|
||||||
|
WHERE app_code = ? AND entitlement_id = ? AND user_id = ?`,
|
||||||
|
resourcedomain.GrantStatusRevoked, nowMs, command.AppCode, item.EntitlementID, grant.TargetUserID,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return walletOutboxEvent{}, err
|
||||||
|
}
|
||||||
|
affected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return walletOutboxEvent{}, err
|
||||||
|
}
|
||||||
|
if affected != 1 {
|
||||||
|
return walletOutboxEvent{}, xerr.New(xerr.LedgerConflict, "resource grant entitlement is not reversible")
|
||||||
|
}
|
||||||
|
if _, err := tx.ExecContext(ctx, `
|
||||||
|
DELETE FROM user_resource_equipment
|
||||||
|
WHERE app_code = ? AND user_id = ? AND entitlement_id = ?`,
|
||||||
|
command.AppCode, grant.TargetUserID, item.EntitlementID,
|
||||||
|
); err != nil {
|
||||||
|
return walletOutboxEvent{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
eventCommandID := resourceGrantRevokeCommandID(command, grant.GrantID, item.GrantItemID)
|
||||||
|
return resourceOutboxEvent("UserResourceChanged", eventCommandID, grant.TargetUserID, item.ResourceID, map[string]any{
|
||||||
|
"action": "revoke",
|
||||||
|
"app_code": command.AppCode,
|
||||||
|
"user_id": grant.TargetUserID,
|
||||||
|
"grant_id": grant.GrantID,
|
||||||
|
"grant_item_id": item.GrantItemID,
|
||||||
|
"resource_id": item.ResourceID,
|
||||||
|
"entitlement_id": item.EntitlementID,
|
||||||
|
"revoked_by_user_id": command.OperatorUserID,
|
||||||
|
"revoke_reason": command.Reason,
|
||||||
|
"updated_at_ms": nowMs,
|
||||||
|
"event_command": eventCommandID,
|
||||||
|
}, nowMs), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Repository) lockOriginalGrantCreditEntry(ctx context.Context, tx *sql.Tx, transactionID string, userID int64) (walletEntry, error) {
|
||||||
|
row := tx.QueryRowContext(ctx, `
|
||||||
|
SELECT asset_type, available_delta, frozen_delta
|
||||||
|
FROM wallet_entries
|
||||||
|
WHERE app_code = ? AND transaction_id = ? AND user_id = ?
|
||||||
|
ORDER BY entry_id DESC LIMIT 1
|
||||||
|
FOR UPDATE`,
|
||||||
|
appcode.FromContext(ctx), transactionID, userID,
|
||||||
|
)
|
||||||
|
var entry walletEntry
|
||||||
|
entry.TransactionID = transactionID
|
||||||
|
entry.UserID = userID
|
||||||
|
if err := row.Scan(&entry.AssetType, &entry.AvailableDelta, &entry.FrozenDelta); err != nil {
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return walletEntry{}, xerr.New(xerr.LedgerConflict, "resource grant wallet entry not found")
|
||||||
|
}
|
||||||
|
return walletEntry{}, err
|
||||||
|
}
|
||||||
|
return entry, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) insertResourceGrantItem(ctx context.Context, tx *sql.Tx, item resourcedomain.ResourceGrantItem) (resourcedomain.ResourceGrantItem, error) {
|
func (r *Repository) insertResourceGrantItem(ctx context.Context, tx *sql.Tx, item resourcedomain.ResourceGrantItem) (resourcedomain.ResourceGrantItem, error) {
|
||||||
result, err := tx.ExecContext(ctx, `
|
result, err := tx.ExecContext(ctx, `
|
||||||
INSERT INTO resource_grant_items (
|
INSERT INTO resource_grant_items (
|
||||||
@ -420,6 +653,11 @@ func (r *Repository) getResourceGrantTx(ctx context.Context, tx *sql.Tx, grantID
|
|||||||
return grant, err
|
return grant, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *Repository) lockResourceGrantTx(ctx context.Context, tx *sql.Tx, grantID string) (resourcedomain.ResourceGrant, error) {
|
||||||
|
row := tx.QueryRowContext(ctx, resourceGrantSelectSQL()+`WHERE app_code = ? AND grant_id = ? FOR UPDATE`, appcode.FromContext(ctx), grantID)
|
||||||
|
return scanResourceGrant(row)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Repository) listResourceGrantItems(ctx context.Context, grantID string) ([]resourcedomain.ResourceGrantItem, error) {
|
func (r *Repository) listResourceGrantItems(ctx context.Context, grantID string) ([]resourcedomain.ResourceGrantItem, error) {
|
||||||
return r.listResourceGrantItemsWithQuery(ctx, r.db, grantID)
|
return r.listResourceGrantItemsWithQuery(ctx, r.db, grantID)
|
||||||
}
|
}
|
||||||
@ -467,6 +705,9 @@ func scanResourceGrant(scanner scanTarget) (resourcedomain.ResourceGrant, error)
|
|||||||
&grant.Status,
|
&grant.Status,
|
||||||
&grant.Reason,
|
&grant.Reason,
|
||||||
&grant.OperatorUserID,
|
&grant.OperatorUserID,
|
||||||
|
&grant.RevokedAtMS,
|
||||||
|
&grant.RevokedByUserID,
|
||||||
|
&grant.RevokeReason,
|
||||||
&grant.CreatedAtMS,
|
&grant.CreatedAtMS,
|
||||||
&grant.UpdatedAtMS,
|
&grant.UpdatedAtMS,
|
||||||
)
|
)
|
||||||
@ -476,7 +717,8 @@ func scanResourceGrant(scanner scanTarget) (resourcedomain.ResourceGrant, error)
|
|||||||
func resourceGrantSelectSQL() string {
|
func resourceGrantSelectSQL() string {
|
||||||
return `
|
return `
|
||||||
SELECT app_code, grant_id, command_id, target_user_id, grant_source, grant_subject_type,
|
SELECT app_code, grant_id, command_id, target_user_id, grant_source, grant_subject_type,
|
||||||
grant_subject_id, status, reason, operator_user_id, created_at_ms, updated_at_ms
|
grant_subject_id, status, reason, operator_user_id,
|
||||||
|
revoked_at_ms, revoked_by_user_id, revoke_reason, created_at_ms, updated_at_ms
|
||||||
FROM resource_grants
|
FROM resource_grants
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
@ -524,6 +766,20 @@ func validateGrantResourceGroupCommand(command resourcedomain.GrantResourceGroup
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeRevokeResourceGrantCommand(command resourcedomain.RevokeResourceGrantCommand) resourcedomain.RevokeResourceGrantCommand {
|
||||||
|
command.RequestID = strings.TrimSpace(command.RequestID)
|
||||||
|
command.GrantID = strings.TrimSpace(command.GrantID)
|
||||||
|
command.Reason = strings.TrimSpace(command.Reason)
|
||||||
|
return command
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateRevokeResourceGrantCommand(command resourcedomain.RevokeResourceGrantCommand) error {
|
||||||
|
if command.GrantID == "" || command.OperatorUserID <= 0 {
|
||||||
|
return xerr.New(xerr.InvalidArgument, "resource grant revoke command is incomplete")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func validateGrantableResourceForSource(resource resourcedomain.Resource, grantSource string) error {
|
func validateGrantableResourceForSource(resource resourcedomain.Resource, grantSource string) error {
|
||||||
|
|
||||||
if grantSource == resourcedomain.GrantSourceGameRobotInit {
|
if grantSource == resourcedomain.GrantSourceGameRobotInit {
|
||||||
@ -579,6 +835,21 @@ func groupWalletAssetCreditHash(grantID string, groupItemID int64, assetType str
|
|||||||
return stableHash(fmt.Sprintf("resource_group_wallet_asset_credit|%s|%d|%s|%d", grantID, groupItemID, resourcedomain.NormalizeWalletAssetType(assetType), amount))
|
return stableHash(fmt.Sprintf("resource_group_wallet_asset_credit|%s|%d|%s|%d", grantID, groupItemID, resourcedomain.NormalizeWalletAssetType(assetType), amount))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resourceGrantWalletRevokeHash(grantID string, grantItemID int64, assetType string, amount int64) string {
|
||||||
|
return stableHash(fmt.Sprintf("resource_grant_wallet_revoke|%s|%d|%s|%d", grantID, grantItemID, resourcedomain.NormalizeWalletAssetType(assetType), amount))
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceGrantRevokeCommandID(command resourcedomain.RevokeResourceGrantCommand, grantID string, grantItemID int64) string {
|
||||||
|
base := strings.TrimSpace(command.RequestID)
|
||||||
|
if base == "" {
|
||||||
|
base = strings.TrimSpace(grantID)
|
||||||
|
}
|
||||||
|
if grantItemID > 0 {
|
||||||
|
return fmt.Sprintf("resource_grant_revoke:%s:item:%d", base, grantItemID)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("resource_grant_revoke:%s", base)
|
||||||
|
}
|
||||||
|
|
||||||
func resourceGrantID(appCode string, commandID string) string {
|
func resourceGrantID(appCode string, commandID string) string {
|
||||||
return "rgr_" + stableHash(appcode.Normalize(appCode)+"|"+commandID)
|
return "rgr_" + stableHash(appcode.Normalize(appCode)+"|"+commandID)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,6 +25,25 @@ func ensureResourceShopPurchaseOrderIndexes(ctx context.Context, db *sql.DB) err
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensureResourceGrantRevokeSchema(ctx context.Context, db *sql.DB) error {
|
||||||
|
if _, err := db.ExecContext(ctx, `
|
||||||
|
ALTER TABLE resource_grants
|
||||||
|
ADD COLUMN revoked_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '撤销时间,UTC epoch ms' AFTER operator_user_id`); err != nil && !isDuplicateColumnError(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := db.ExecContext(ctx, `
|
||||||
|
ALTER TABLE resource_grants
|
||||||
|
ADD COLUMN revoked_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '撤销操作人用户 ID' AFTER revoked_at_ms`); err != nil && !isDuplicateColumnError(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := db.ExecContext(ctx, `
|
||||||
|
ALTER TABLE resource_grants
|
||||||
|
ADD COLUMN revoke_reason VARCHAR(256) NOT NULL DEFAULT '' COMMENT '撤销原因' AFTER revoked_by_user_id`); err != nil && !isDuplicateColumnError(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func isDuplicateKeyNameError(err error) bool {
|
func isDuplicateKeyNameError(err error) bool {
|
||||||
var mysqlErr *mysqlDriver.MySQLError
|
var mysqlErr *mysqlDriver.MySQLError
|
||||||
return errors.As(err, &mysqlErr) && mysqlErr.Number == 1061
|
return errors.As(err, &mysqlErr) && mysqlErr.Number == 1061
|
||||||
|
|||||||
@ -89,6 +89,20 @@ func (r *Repository) ExpireEntitlement(entitlementID string, expiresAtMS int64)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetResourceGrantStatus lets service tests model legacy or abnormal grant rows without bypassing production grant creation.
|
||||||
|
func (r *Repository) SetResourceGrantStatus(grantID string, status string) {
|
||||||
|
r.t.Helper()
|
||||||
|
|
||||||
|
if _, err := r.schema.DB.ExecContext(context.Background(), `
|
||||||
|
UPDATE resource_grants
|
||||||
|
SET status = ?, updated_at_ms = ?
|
||||||
|
WHERE grant_id = ?`,
|
||||||
|
status, time.Now().UnixMilli(), grantID,
|
||||||
|
); err != nil {
|
||||||
|
r.t.Fatalf("set resource grant status failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// HostSalarySettlementIDs returns the persisted settlement IDs for one host/cycle in creation order.
|
// HostSalarySettlementIDs returns the persisted settlement IDs for one host/cycle in creation order.
|
||||||
func (r *Repository) HostSalarySettlementIDs(userID int64, cycleKey string) []string {
|
func (r *Repository) HostSalarySettlementIDs(userID int64, cycleKey string) []string {
|
||||||
r.t.Helper()
|
r.t.Helper()
|
||||||
|
|||||||
@ -250,6 +250,20 @@ func (s *Server) GrantResourceGroup(ctx context.Context, req *walletv1.GrantReso
|
|||||||
return &walletv1.ResourceGrantResponse{Grant: resourceGrantToProto(grant)}, nil
|
return &walletv1.ResourceGrantResponse{Grant: resourceGrantToProto(grant)}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Server) RevokeResourceGrant(ctx context.Context, req *walletv1.RevokeResourceGrantRequest) (*walletv1.ResourceGrantResponse, error) {
|
||||||
|
grant, err := s.svc.RevokeResourceGrant(ctx, resourcedomain.RevokeResourceGrantCommand{
|
||||||
|
AppCode: req.GetAppCode(),
|
||||||
|
RequestID: req.GetRequestId(),
|
||||||
|
GrantID: req.GetGrantId(),
|
||||||
|
Reason: req.GetReason(),
|
||||||
|
OperatorUserID: req.GetOperatorUserId(),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, xerr.ToGRPCError(err)
|
||||||
|
}
|
||||||
|
return &walletv1.ResourceGrantResponse{Grant: resourceGrantToProto(grant)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Server) ListUserResources(ctx context.Context, req *walletv1.ListUserResourcesRequest) (*walletv1.ListUserResourcesResponse, error) {
|
func (s *Server) ListUserResources(ctx context.Context, req *walletv1.ListUserResourcesRequest) (*walletv1.ListUserResourcesResponse, error) {
|
||||||
items, err := s.svc.ListUserResources(ctx, resourcedomain.ListUserResourcesQuery{
|
items, err := s.svc.ListUserResources(ctx, resourcedomain.ListUserResourcesQuery{
|
||||||
AppCode: req.GetAppCode(),
|
AppCode: req.GetAppCode(),
|
||||||
|
|||||||
@ -221,5 +221,8 @@ func resourceGrantToProto(grant resourcedomain.ResourceGrant) *walletv1.Resource
|
|||||||
Items: items,
|
Items: items,
|
||||||
CreatedAtMs: grant.CreatedAtMS,
|
CreatedAtMs: grant.CreatedAtMS,
|
||||||
UpdatedAtMs: grant.UpdatedAtMS,
|
UpdatedAtMs: grant.UpdatedAtMS,
|
||||||
|
RevokedAtMs: grant.RevokedAtMS,
|
||||||
|
RevokedByUserId: grant.RevokedByUserID,
|
||||||
|
RevokeReason: grant.RevokeReason,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user