批量删除

This commit is contained in:
zhx 2026-07-05 14:55:37 +08:00
parent 6c3841248e
commit 15810e891d
17 changed files with 5300 additions and 4114 deletions

File diff suppressed because it is too large Load Diff

View File

@ -672,6 +672,24 @@ message SetResourceStatusRequest {
int64 operator_user_id = 5;
}
message DeleteResourceRequest {
string request_id = 1;
string app_code = 2;
int64 resource_id = 3;
int64 operator_user_id = 4;
}
message BatchDeleteResourcesRequest {
string request_id = 1;
string app_code = 2;
repeated int64 resource_ids = 3;
int64 operator_user_id = 4;
}
message BatchDeleteResourcesResponse {
repeated Resource resources = 1;
}
message ResourceResponse {
Resource resource = 1;
}
@ -807,6 +825,17 @@ message CreateGiftConfigRequest {
string cp_relation_type = 21;
}
message BatchCreateGiftConfigsRequest {
string request_id = 1;
string app_code = 2;
repeated CreateGiftConfigRequest items = 3;
int64 operator_user_id = 4;
}
message BatchCreateGiftConfigsResponse {
repeated GiftConfig gifts = 1;
}
message UpdateGiftConfigRequest {
string request_id = 1;
string app_code = 2;
@ -2262,6 +2291,8 @@ service WalletService {
rpc CreateResource(CreateResourceRequest) returns (ResourceResponse);
rpc UpdateResource(UpdateResourceRequest) returns (ResourceResponse);
rpc SetResourceStatus(SetResourceStatusRequest) returns (ResourceResponse);
rpc DeleteResource(DeleteResourceRequest) returns (ResourceResponse);
rpc BatchDeleteResources(BatchDeleteResourcesRequest) returns (BatchDeleteResourcesResponse);
rpc ListResourceGroups(ListResourceGroupsRequest) returns (ListResourceGroupsResponse);
rpc GetResourceGroup(GetResourceGroupRequest) returns (GetResourceGroupResponse);
rpc CreateResourceGroup(CreateResourceGroupRequest) returns (ResourceGroupResponse);
@ -2271,6 +2302,7 @@ service WalletService {
rpc ListGiftTypeConfigs(ListGiftTypeConfigsRequest) returns (ListGiftTypeConfigsResponse);
rpc GetGiftCatalogVersion(GetGiftCatalogVersionRequest) returns (GetGiftCatalogVersionResponse);
rpc CreateGiftConfig(CreateGiftConfigRequest) returns (GiftConfigResponse);
rpc BatchCreateGiftConfigs(BatchCreateGiftConfigsRequest) returns (BatchCreateGiftConfigsResponse);
rpc UpdateGiftConfig(UpdateGiftConfigRequest) returns (GiftConfigResponse);
rpc SetGiftConfigStatus(SetGiftConfigStatusRequest) returns (GiftConfigResponse);
rpc DeleteGiftConfig(DeleteGiftConfigRequest) returns (GiftConfigResponse);

View File

@ -222,6 +222,8 @@ const (
WalletService_CreateResource_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateResource"
WalletService_UpdateResource_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateResource"
WalletService_SetResourceStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetResourceStatus"
WalletService_DeleteResource_FullMethodName = "/hyapp.wallet.v1.WalletService/DeleteResource"
WalletService_BatchDeleteResources_FullMethodName = "/hyapp.wallet.v1.WalletService/BatchDeleteResources"
WalletService_ListResourceGroups_FullMethodName = "/hyapp.wallet.v1.WalletService/ListResourceGroups"
WalletService_GetResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/GetResourceGroup"
WalletService_CreateResourceGroup_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateResourceGroup"
@ -231,6 +233,7 @@ const (
WalletService_ListGiftTypeConfigs_FullMethodName = "/hyapp.wallet.v1.WalletService/ListGiftTypeConfigs"
WalletService_GetGiftCatalogVersion_FullMethodName = "/hyapp.wallet.v1.WalletService/GetGiftCatalogVersion"
WalletService_CreateGiftConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/CreateGiftConfig"
WalletService_BatchCreateGiftConfigs_FullMethodName = "/hyapp.wallet.v1.WalletService/BatchCreateGiftConfigs"
WalletService_UpdateGiftConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/UpdateGiftConfig"
WalletService_SetGiftConfigStatus_FullMethodName = "/hyapp.wallet.v1.WalletService/SetGiftConfigStatus"
WalletService_DeleteGiftConfig_FullMethodName = "/hyapp.wallet.v1.WalletService/DeleteGiftConfig"
@ -328,6 +331,8 @@ type WalletServiceClient interface {
CreateResource(ctx context.Context, in *CreateResourceRequest, opts ...grpc.CallOption) (*ResourceResponse, error)
UpdateResource(ctx context.Context, in *UpdateResourceRequest, opts ...grpc.CallOption) (*ResourceResponse, error)
SetResourceStatus(ctx context.Context, in *SetResourceStatusRequest, opts ...grpc.CallOption) (*ResourceResponse, error)
DeleteResource(ctx context.Context, in *DeleteResourceRequest, opts ...grpc.CallOption) (*ResourceResponse, error)
BatchDeleteResources(ctx context.Context, in *BatchDeleteResourcesRequest, opts ...grpc.CallOption) (*BatchDeleteResourcesResponse, error)
ListResourceGroups(ctx context.Context, in *ListResourceGroupsRequest, opts ...grpc.CallOption) (*ListResourceGroupsResponse, error)
GetResourceGroup(ctx context.Context, in *GetResourceGroupRequest, opts ...grpc.CallOption) (*GetResourceGroupResponse, error)
CreateResourceGroup(ctx context.Context, in *CreateResourceGroupRequest, opts ...grpc.CallOption) (*ResourceGroupResponse, error)
@ -337,6 +342,7 @@ type WalletServiceClient interface {
ListGiftTypeConfigs(ctx context.Context, in *ListGiftTypeConfigsRequest, opts ...grpc.CallOption) (*ListGiftTypeConfigsResponse, error)
GetGiftCatalogVersion(ctx context.Context, in *GetGiftCatalogVersionRequest, opts ...grpc.CallOption) (*GetGiftCatalogVersionResponse, error)
CreateGiftConfig(ctx context.Context, in *CreateGiftConfigRequest, opts ...grpc.CallOption) (*GiftConfigResponse, error)
BatchCreateGiftConfigs(ctx context.Context, in *BatchCreateGiftConfigsRequest, opts ...grpc.CallOption) (*BatchCreateGiftConfigsResponse, error)
UpdateGiftConfig(ctx context.Context, in *UpdateGiftConfigRequest, opts ...grpc.CallOption) (*GiftConfigResponse, error)
SetGiftConfigStatus(ctx context.Context, in *SetGiftConfigStatusRequest, opts ...grpc.CallOption) (*GiftConfigResponse, error)
DeleteGiftConfig(ctx context.Context, in *DeleteGiftConfigRequest, opts ...grpc.CallOption) (*GiftConfigResponse, error)
@ -625,6 +631,26 @@ func (c *walletServiceClient) SetResourceStatus(ctx context.Context, in *SetReso
return out, nil
}
func (c *walletServiceClient) DeleteResource(ctx context.Context, in *DeleteResourceRequest, opts ...grpc.CallOption) (*ResourceResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ResourceResponse)
err := c.cc.Invoke(ctx, WalletService_DeleteResource_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) BatchDeleteResources(ctx context.Context, in *BatchDeleteResourcesRequest, opts ...grpc.CallOption) (*BatchDeleteResourcesResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(BatchDeleteResourcesResponse)
err := c.cc.Invoke(ctx, WalletService_BatchDeleteResources_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) ListResourceGroups(ctx context.Context, in *ListResourceGroupsRequest, opts ...grpc.CallOption) (*ListResourceGroupsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(ListResourceGroupsResponse)
@ -715,6 +741,16 @@ func (c *walletServiceClient) CreateGiftConfig(ctx context.Context, in *CreateGi
return out, nil
}
func (c *walletServiceClient) BatchCreateGiftConfigs(ctx context.Context, in *BatchCreateGiftConfigsRequest, opts ...grpc.CallOption) (*BatchCreateGiftConfigsResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(BatchCreateGiftConfigsResponse)
err := c.cc.Invoke(ctx, WalletService_BatchCreateGiftConfigs_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *walletServiceClient) UpdateGiftConfig(ctx context.Context, in *UpdateGiftConfigRequest, opts ...grpc.CallOption) (*GiftConfigResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(GiftConfigResponse)
@ -1422,6 +1458,8 @@ type WalletServiceServer interface {
CreateResource(context.Context, *CreateResourceRequest) (*ResourceResponse, error)
UpdateResource(context.Context, *UpdateResourceRequest) (*ResourceResponse, error)
SetResourceStatus(context.Context, *SetResourceStatusRequest) (*ResourceResponse, error)
DeleteResource(context.Context, *DeleteResourceRequest) (*ResourceResponse, error)
BatchDeleteResources(context.Context, *BatchDeleteResourcesRequest) (*BatchDeleteResourcesResponse, error)
ListResourceGroups(context.Context, *ListResourceGroupsRequest) (*ListResourceGroupsResponse, error)
GetResourceGroup(context.Context, *GetResourceGroupRequest) (*GetResourceGroupResponse, error)
CreateResourceGroup(context.Context, *CreateResourceGroupRequest) (*ResourceGroupResponse, error)
@ -1431,6 +1469,7 @@ type WalletServiceServer interface {
ListGiftTypeConfigs(context.Context, *ListGiftTypeConfigsRequest) (*ListGiftTypeConfigsResponse, error)
GetGiftCatalogVersion(context.Context, *GetGiftCatalogVersionRequest) (*GetGiftCatalogVersionResponse, error)
CreateGiftConfig(context.Context, *CreateGiftConfigRequest) (*GiftConfigResponse, error)
BatchCreateGiftConfigs(context.Context, *BatchCreateGiftConfigsRequest) (*BatchCreateGiftConfigsResponse, error)
UpdateGiftConfig(context.Context, *UpdateGiftConfigRequest) (*GiftConfigResponse, error)
SetGiftConfigStatus(context.Context, *SetGiftConfigStatusRequest) (*GiftConfigResponse, error)
DeleteGiftConfig(context.Context, *DeleteGiftConfigRequest) (*GiftConfigResponse, error)
@ -1572,6 +1611,12 @@ func (UnimplementedWalletServiceServer) UpdateResource(context.Context, *UpdateR
func (UnimplementedWalletServiceServer) SetResourceStatus(context.Context, *SetResourceStatusRequest) (*ResourceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method SetResourceStatus not implemented")
}
func (UnimplementedWalletServiceServer) DeleteResource(context.Context, *DeleteResourceRequest) (*ResourceResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method DeleteResource not implemented")
}
func (UnimplementedWalletServiceServer) BatchDeleteResources(context.Context, *BatchDeleteResourcesRequest) (*BatchDeleteResourcesResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method BatchDeleteResources not implemented")
}
func (UnimplementedWalletServiceServer) ListResourceGroups(context.Context, *ListResourceGroupsRequest) (*ListResourceGroupsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method ListResourceGroups not implemented")
}
@ -1599,6 +1644,9 @@ func (UnimplementedWalletServiceServer) GetGiftCatalogVersion(context.Context, *
func (UnimplementedWalletServiceServer) CreateGiftConfig(context.Context, *CreateGiftConfigRequest) (*GiftConfigResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method CreateGiftConfig not implemented")
}
func (UnimplementedWalletServiceServer) BatchCreateGiftConfigs(context.Context, *BatchCreateGiftConfigsRequest) (*BatchCreateGiftConfigsResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method BatchCreateGiftConfigs not implemented")
}
func (UnimplementedWalletServiceServer) UpdateGiftConfig(context.Context, *UpdateGiftConfigRequest) (*GiftConfigResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method UpdateGiftConfig not implemented")
}
@ -2202,6 +2250,42 @@ func _WalletService_SetResourceStatus_Handler(srv interface{}, ctx context.Conte
return interceptor(ctx, in, info, handler)
}
func _WalletService_DeleteResource_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(DeleteResourceRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).DeleteResource(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_DeleteResource_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).DeleteResource(ctx, req.(*DeleteResourceRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_BatchDeleteResources_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(BatchDeleteResourcesRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).BatchDeleteResources(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_BatchDeleteResources_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).BatchDeleteResources(ctx, req.(*BatchDeleteResourcesRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_ListResourceGroups_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(ListResourceGroupsRequest)
if err := dec(in); err != nil {
@ -2364,6 +2448,24 @@ func _WalletService_CreateGiftConfig_Handler(srv interface{}, ctx context.Contex
return interceptor(ctx, in, info, handler)
}
func _WalletService_BatchCreateGiftConfigs_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(BatchCreateGiftConfigsRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).BatchCreateGiftConfigs(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_BatchCreateGiftConfigs_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).BatchCreateGiftConfigs(ctx, req.(*BatchCreateGiftConfigsRequest))
}
return interceptor(ctx, in, info, handler)
}
func _WalletService_UpdateGiftConfig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(UpdateGiftConfigRequest)
if err := dec(in); err != nil {
@ -3679,6 +3781,14 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
MethodName: "SetResourceStatus",
Handler: _WalletService_SetResourceStatus_Handler,
},
{
MethodName: "DeleteResource",
Handler: _WalletService_DeleteResource_Handler,
},
{
MethodName: "BatchDeleteResources",
Handler: _WalletService_BatchDeleteResources_Handler,
},
{
MethodName: "ListResourceGroups",
Handler: _WalletService_ListResourceGroups_Handler,
@ -3715,6 +3825,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
MethodName: "CreateGiftConfig",
Handler: _WalletService_CreateGiftConfig_Handler,
},
{
MethodName: "BatchCreateGiftConfigs",
Handler: _WalletService_BatchCreateGiftConfigs_Handler,
},
{
MethodName: "UpdateGiftConfig",
Handler: _WalletService_UpdateGiftConfig_Handler,

View File

@ -15,6 +15,8 @@ type Client interface {
CreateResource(ctx context.Context, req *walletv1.CreateResourceRequest) (*walletv1.ResourceResponse, error)
UpdateResource(ctx context.Context, req *walletv1.UpdateResourceRequest) (*walletv1.ResourceResponse, error)
SetResourceStatus(ctx context.Context, req *walletv1.SetResourceStatusRequest) (*walletv1.ResourceResponse, error)
DeleteResource(ctx context.Context, req *walletv1.DeleteResourceRequest) (*walletv1.ResourceResponse, error)
BatchDeleteResources(ctx context.Context, req *walletv1.BatchDeleteResourcesRequest) (*walletv1.BatchDeleteResourcesResponse, error)
ListResourceGroups(ctx context.Context, req *walletv1.ListResourceGroupsRequest) (*walletv1.ListResourceGroupsResponse, error)
GetResourceGroup(ctx context.Context, req *walletv1.GetResourceGroupRequest) (*walletv1.GetResourceGroupResponse, error)
CreateResourceGroup(ctx context.Context, req *walletv1.CreateResourceGroupRequest) (*walletv1.ResourceGroupResponse, error)
@ -23,6 +25,7 @@ type Client interface {
ListGiftConfigs(ctx context.Context, req *walletv1.ListGiftConfigsRequest) (*walletv1.ListGiftConfigsResponse, error)
ListGiftTypeConfigs(ctx context.Context, req *walletv1.ListGiftTypeConfigsRequest) (*walletv1.ListGiftTypeConfigsResponse, error)
CreateGiftConfig(ctx context.Context, req *walletv1.CreateGiftConfigRequest) (*walletv1.GiftConfigResponse, error)
BatchCreateGiftConfigs(ctx context.Context, req *walletv1.BatchCreateGiftConfigsRequest) (*walletv1.BatchCreateGiftConfigsResponse, error)
UpdateGiftConfig(ctx context.Context, req *walletv1.UpdateGiftConfigRequest) (*walletv1.GiftConfigResponse, error)
SetGiftConfigStatus(ctx context.Context, req *walletv1.SetGiftConfigStatusRequest) (*walletv1.GiftConfigResponse, error)
DeleteGiftConfig(ctx context.Context, req *walletv1.DeleteGiftConfigRequest) (*walletv1.GiftConfigResponse, error)
@ -93,6 +96,14 @@ func (c *GRPCClient) SetResourceStatus(ctx context.Context, req *walletv1.SetRes
return c.client.SetResourceStatus(ctx, req)
}
func (c *GRPCClient) DeleteResource(ctx context.Context, req *walletv1.DeleteResourceRequest) (*walletv1.ResourceResponse, error) {
return c.client.DeleteResource(ctx, req)
}
func (c *GRPCClient) BatchDeleteResources(ctx context.Context, req *walletv1.BatchDeleteResourcesRequest) (*walletv1.BatchDeleteResourcesResponse, error) {
return c.client.BatchDeleteResources(ctx, req)
}
func (c *GRPCClient) ListResourceGroups(ctx context.Context, req *walletv1.ListResourceGroupsRequest) (*walletv1.ListResourceGroupsResponse, error) {
return c.client.ListResourceGroups(ctx, req)
}
@ -125,6 +136,10 @@ func (c *GRPCClient) CreateGiftConfig(ctx context.Context, req *walletv1.CreateG
return c.client.CreateGiftConfig(ctx, req)
}
func (c *GRPCClient) BatchCreateGiftConfigs(ctx context.Context, req *walletv1.BatchCreateGiftConfigsRequest) (*walletv1.BatchCreateGiftConfigsResponse, error) {
return c.client.BatchCreateGiftConfigs(ctx, req)
}
func (c *GRPCClient) UpdateGiftConfig(ctx context.Context, req *walletv1.UpdateGiftConfigRequest) (*walletv1.GiftConfigResponse, error) {
return c.client.UpdateGiftConfig(ctx, req)
}

View File

@ -231,6 +231,53 @@ func (h *Handler) UpdateResource(c *gin.Context) {
response.OK(c, resource)
}
func (h *Handler) DeleteResource(c *gin.Context) {
resourceID, ok := parseID(c, "resource_id")
if !ok {
return
}
resp, err := h.wallet.DeleteResource(c.Request.Context(), &walletv1.DeleteResourceRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
ResourceId: resourceID,
OperatorUserId: actorID(c),
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
resource := resourceFromProto(resp.GetResource())
h.auditLog(c, "delete-resource", "resources", fmt.Sprintf("%d", resource.ResourceID), "success", resource.ResourceCode)
response.OK(c, resource)
}
func (h *Handler) BatchDeleteResources(c *gin.Context) {
var req resourceDeleteBatchRequest
if err := c.ShouldBindJSON(&req); err != nil || len(req.ResourceIDs) == 0 {
response.BadRequest(c, "资源参数不正确")
return
}
resp, err := h.wallet.BatchDeleteResources(c.Request.Context(), &walletv1.BatchDeleteResourcesRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
ResourceIds: req.ResourceIDs,
OperatorUserId: actorID(c),
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
items := make([]resourceDTO, 0, len(resp.GetResources()))
deletedIDs := make([]string, 0, len(resp.GetResources()))
for _, item := range resp.GetResources() {
resource := resourceFromProto(item)
items = append(items, resource)
deletedIDs = append(deletedIDs, fmt.Sprintf("%d", resource.ResourceID))
}
h.auditLog(c, "delete-resources", "resources", "batch", "success", strings.Join(deletedIDs, ","))
response.OK(c, items)
}
func (h *Handler) UpdateMP4ResourceLayouts(c *gin.Context) {
var req resourceMP4LayoutBatchRequest
if err := c.ShouldBindJSON(&req); err != nil {
@ -500,6 +547,37 @@ func (h *Handler) CreateGift(c *gin.Context) {
response.Created(c, gift)
}
func (h *Handler) BatchCreateGifts(c *gin.Context) {
var req giftBatchRequest
if err := c.ShouldBindJSON(&req); err != nil || len(req.Items) == 0 {
response.BadRequest(c, "礼物参数不正确")
return
}
items := make([]*walletv1.CreateGiftConfigRequest, 0, len(req.Items))
for _, item := range req.Items {
items = append(items, item.createProto(c))
}
resp, err := h.wallet.BatchCreateGiftConfigs(c.Request.Context(), &walletv1.BatchCreateGiftConfigsRequest{
RequestId: middleware.CurrentRequestID(c),
AppCode: appctx.FromContext(c.Request.Context()),
Items: items,
OperatorUserId: actorID(c),
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
gifts := make([]giftDTO, 0, len(resp.GetGifts()))
giftIDs := make([]string, 0, len(resp.GetGifts()))
for _, item := range resp.GetGifts() {
gift := giftFromProto(item)
gifts = append(gifts, gift)
giftIDs = append(giftIDs, gift.GiftID)
}
h.auditLog(c, "create-gifts", "gift_configs", "batch", "success", strings.Join(giftIDs, ","))
response.Created(c, gifts)
}
func (h *Handler) UpdateGift(c *gin.Context) {
giftID := strings.TrimSpace(c.Param("gift_id"))
if giftID == "" {

View File

@ -136,6 +136,74 @@ func TestDeleteGiftOnlyCallsGiftConfigDelete(t *testing.T) {
}
}
func TestDeleteResourceCallsWalletDeleteResource(t *testing.T) {
wallet := &mockResourceWallet{resources: map[int64]*walletv1.Resource{
11: {AppCode: "lalu", ResourceId: 11, ResourceCode: "rose_resource", ResourceType: "gift", Name: "Rose"},
}}
router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodDelete, "/admin/resources/11", nil)
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("delete resource status mismatch: %d %s", recorder.Code, recorder.Body.String())
}
if len(wallet.deletedResources) != 1 {
t.Fatalf("expected one resource delete request, got %d", len(wallet.deletedResources))
}
deleteReq := wallet.deletedResources[0]
if deleteReq.GetAppCode() != "lalu" || deleteReq.GetResourceId() != 11 || deleteReq.GetOperatorUserId() != 7 {
t.Fatalf("delete resource request mismatch: %+v", deleteReq)
}
}
func TestBatchDeleteResourcesCallsWalletBatchDelete(t *testing.T) {
wallet := &mockResourceWallet{resources: map[int64]*walletv1.Resource{
11: {AppCode: "lalu", ResourceId: 11, ResourceCode: "rose_resource", ResourceType: "gift", Name: "Rose"},
12: {AppCode: "lalu", ResourceId: 12, ResourceCode: "ring_resource", ResourceType: "gift", Name: "Ring"},
}}
router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/admin/resources/batch-delete", strings.NewReader(`{"resourceIds":[11,12]}`))
request.Header.Set("Content-Type", "application/json")
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("batch delete resources status mismatch: %d %s", recorder.Code, recorder.Body.String())
}
if len(wallet.batchDeletedResources) != 1 {
t.Fatalf("expected one batch resource delete request, got %d", len(wallet.batchDeletedResources))
}
deleteReq := wallet.batchDeletedResources[0]
if deleteReq.GetAppCode() != "lalu" || fmt.Sprint(deleteReq.GetResourceIds()) != "[11 12]" || deleteReq.GetOperatorUserId() != 7 {
t.Fatalf("batch delete resource request mismatch: %+v", deleteReq)
}
}
func TestBatchCreateGiftsCallsWalletBatchCreate(t *testing.T) {
wallet := &mockResourceWallet{}
router := newResourceHandlerTestRouter(New(wallet, nil, nil, time.Second, nil))
body := `{"items":[{"giftId":"rose","resourceId":11,"status":"active","name":"Rose","priceVersion":"default","giftTypeCode":"normal","chargeAssetType":"COIN","coinPrice":10,"regionIds":[0]},{"giftId":"ring","resourceId":12,"status":"active","name":"Ring","priceVersion":"default","giftTypeCode":"normal","chargeAssetType":"COIN","coinPrice":20,"regionIds":[0]}]}`
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodPost, "/admin/gifts/batch", strings.NewReader(body))
request.Header.Set("Content-Type", "application/json")
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusCreated {
t.Fatalf("batch create gifts status mismatch: %d %s", recorder.Code, recorder.Body.String())
}
if len(wallet.batchCreatedGifts) != 1 {
t.Fatalf("expected one batch gift create request, got %d", len(wallet.batchCreatedGifts))
}
createReq := wallet.batchCreatedGifts[0]
if createReq.GetAppCode() != "lalu" || len(createReq.GetItems()) != 2 || createReq.GetItems()[0].GetGiftId() != "rose" || createReq.GetItems()[1].GetGiftId() != "ring" {
t.Fatalf("batch create gift request mismatch: %+v", createReq)
}
}
func TestIdentityAutoGrantConfigRouteDoesNotHitResourceGroupID(t *testing.T) {
router := newResourceHandlerTestRouter(New(&mockResourceWallet{}, nil, nil, time.Second, nil))
@ -215,6 +283,7 @@ func newResourceHandlerTestRouter(handler *Handler) *gin.Engine {
"gift:create",
"gift:status",
"resource:create",
"resource:delete",
"resource-grant:create",
"resource-grant:revoke",
"resource-grant:view",
@ -234,10 +303,13 @@ func newResourceHandlerTestRouter(handler *Handler) *gin.Engine {
type mockResourceWallet struct {
walletclient.Client
resources map[int64]*walletv1.Resource
updates []*walletv1.UpdateResourceRequest
deletedGifts []*walletv1.DeleteGiftConfigRequest
listGrantRequests []*walletv1.ListResourceGrantsRequest
resources map[int64]*walletv1.Resource
updates []*walletv1.UpdateResourceRequest
deletedResources []*walletv1.DeleteResourceRequest
batchDeletedResources []*walletv1.BatchDeleteResourcesRequest
batchCreatedGifts []*walletv1.BatchCreateGiftConfigsRequest
deletedGifts []*walletv1.DeleteGiftConfigRequest
listGrantRequests []*walletv1.ListResourceGrantsRequest
}
func (m *mockResourceWallet) GetResource(ctx context.Context, req *walletv1.GetResourceRequest) (*walletv1.GetResourceResponse, error) {
@ -273,6 +345,43 @@ func (m *mockResourceWallet) UpdateResource(ctx context.Context, req *walletv1.U
}}, nil
}
func (m *mockResourceWallet) DeleteResource(ctx context.Context, req *walletv1.DeleteResourceRequest) (*walletv1.ResourceResponse, error) {
m.deletedResources = append(m.deletedResources, req)
resource := m.resources[req.GetResourceId()]
if resource == nil {
return nil, fmt.Errorf("resource not found")
}
return &walletv1.ResourceResponse{Resource: resource}, nil
}
func (m *mockResourceWallet) BatchDeleteResources(ctx context.Context, req *walletv1.BatchDeleteResourcesRequest) (*walletv1.BatchDeleteResourcesResponse, error) {
m.batchDeletedResources = append(m.batchDeletedResources, req)
resp := &walletv1.BatchDeleteResourcesResponse{Resources: make([]*walletv1.Resource, 0, len(req.GetResourceIds()))}
for _, resourceID := range req.GetResourceIds() {
resource := m.resources[resourceID]
if resource == nil {
return nil, fmt.Errorf("resource not found")
}
resp.Resources = append(resp.Resources, resource)
}
return resp, nil
}
func (m *mockResourceWallet) BatchCreateGiftConfigs(ctx context.Context, req *walletv1.BatchCreateGiftConfigsRequest) (*walletv1.BatchCreateGiftConfigsResponse, error) {
m.batchCreatedGifts = append(m.batchCreatedGifts, req)
resp := &walletv1.BatchCreateGiftConfigsResponse{Gifts: make([]*walletv1.GiftConfig, 0, len(req.GetItems()))}
for _, item := range req.GetItems() {
resp.Gifts = append(resp.Gifts, &walletv1.GiftConfig{
AppCode: item.GetAppCode(),
GiftId: item.GetGiftId(),
ResourceId: item.GetResourceId(),
Status: item.GetStatus(),
Name: item.GetName(),
})
}
return resp, nil
}
func (m *mockResourceWallet) DeleteGiftConfig(ctx context.Context, req *walletv1.DeleteGiftConfigRequest) (*walletv1.GiftConfigResponse, error) {
m.deletedGifts = append(m.deletedGifts, req)
return &walletv1.GiftConfigResponse{Gift: &walletv1.GiftConfig{

View File

@ -89,6 +89,10 @@ type resourceMP4LayoutBatchRequest struct {
Items []resourceMP4LayoutUpdateRequest `json:"items"`
}
type resourceDeleteBatchRequest struct {
ResourceIDs []int64 `json:"resourceIds"`
}
type resourceMP4LayoutUpdateRequest struct {
ResourceID int64 `json:"resourceId"`
MetadataJSON string `json:"metadataJson"`
@ -191,6 +195,10 @@ type giftRequest struct {
RegionIDs []int64 `json:"regionIds"`
}
type giftBatchRequest struct {
Items []giftRequest `json:"items"`
}
type giftTypeRequest struct {
DisplayName string `json:"displayName"`
TabName string `json:"tabName"`

View File

@ -14,8 +14,10 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
protected.GET("/admin/resources", middleware.RequirePermission("resource:view"), h.ListResources)
protected.POST("/admin/resources", middleware.RequirePermission("resource:create"), h.CreateResource)
protected.PUT("/admin/resources/mp4-layouts/batch", middleware.RequirePermission("resource:update"), h.UpdateMP4ResourceLayouts)
protected.POST("/admin/resources/batch-delete", middleware.RequirePermission("resource:delete"), h.BatchDeleteResources)
protected.GET("/admin/resources/:resource_id", middleware.RequirePermission("resource:view"), h.GetResource)
protected.PUT("/admin/resources/:resource_id", middleware.RequirePermission("resource:update"), h.UpdateResource)
protected.DELETE("/admin/resources/:resource_id", middleware.RequirePermission("resource:delete"), h.DeleteResource)
protected.POST("/admin/resources/:resource_id/enable", middleware.RequirePermission("resource:update"), h.EnableResource)
protected.POST("/admin/resources/:resource_id/disable", middleware.RequirePermission("resource:update"), h.DisableResource)
@ -38,6 +40,7 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
protected.PUT("/admin/gift-types", middleware.RequirePermission("gift:update"), h.UpdateGiftTypes)
protected.PUT("/admin/gift-types/:type_code", middleware.RequirePermission("gift:update"), h.UpdateGiftType)
protected.POST("/admin/gifts", middleware.RequirePermission("gift:create"), h.CreateGift)
protected.POST("/admin/gifts/batch", middleware.RequirePermission("gift:create"), h.BatchCreateGifts)
protected.PUT("/admin/gifts/:gift_id", middleware.RequirePermission("gift:update"), h.UpdateGift)
protected.DELETE("/admin/gifts/:gift_id", middleware.RequirePermission("gift:delete"), h.DeleteGift)
protected.POST("/admin/gifts/:gift_id/enable", middleware.RequirePermission("gift:status"), h.EnableGift)

View File

@ -15,6 +15,7 @@ const (
StatusActive = "active"
StatusDisabled = "disabled"
StatusDeleted = "deleted"
GrantStrategyWalletCredit = "wallet_credit"
GrantStrategyNewEntitlement = "new_entitlement"

View File

@ -77,6 +77,12 @@ type StatusCommand struct {
OperatorUserID int64
}
type BatchDeleteResourcesCommand struct {
AppCode string
ResourceIDs []int64
OperatorUserID int64
}
func NormalizeStatus(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
if value == "" {

View File

@ -158,6 +158,8 @@ type ResourceCatalogStore interface {
CreateResource(ctx context.Context, command resourcedomain.ResourceCommand) (resourcedomain.Resource, error)
UpdateResource(ctx context.Context, command resourcedomain.ResourceCommand) (resourcedomain.Resource, error)
SetResourceStatus(ctx context.Context, command resourcedomain.StatusCommand) (resourcedomain.Resource, error)
DeleteResource(ctx context.Context, command resourcedomain.StatusCommand) (resourcedomain.Resource, error)
BatchDeleteResources(ctx context.Context, command resourcedomain.BatchDeleteResourcesCommand) ([]resourcedomain.Resource, error)
ListResourceGroups(ctx context.Context, query resourcedomain.ListResourceGroupsQuery) ([]resourcedomain.ResourceGroup, int64, error)
GetResourceGroup(ctx context.Context, groupID int64) (resourcedomain.ResourceGroup, error)
CreateResourceGroup(ctx context.Context, command resourcedomain.ResourceGroupCommand) (resourcedomain.ResourceGroup, error)
@ -167,6 +169,7 @@ type ResourceCatalogStore interface {
ListGiftTypeConfigs(ctx context.Context, query resourcedomain.ListGiftTypeConfigsQuery) ([]resourcedomain.GiftTypeConfig, error)
GetGiftCatalogVersion(ctx context.Context, appCode string) (int64, error)
CreateGiftConfig(ctx context.Context, command resourcedomain.GiftConfigCommand) (resourcedomain.GiftConfig, error)
BatchCreateGiftConfigs(ctx context.Context, commands []resourcedomain.GiftConfigCommand) ([]resourcedomain.GiftConfig, error)
UpdateGiftConfig(ctx context.Context, command resourcedomain.GiftConfigCommand) (resourcedomain.GiftConfig, error)
SetGiftConfigStatus(ctx context.Context, command resourcedomain.StatusCommand) (resourcedomain.GiftConfig, error)
DeleteGiftConfig(ctx context.Context, command resourcedomain.StatusCommand) (resourcedomain.GiftConfig, error)

View File

@ -59,6 +59,24 @@ func (s *Service) SetResourceStatus(ctx context.Context, command resourcedomain.
return s.repository.SetResourceStatus(ctx, command)
}
func (s *Service) DeleteResource(ctx context.Context, command resourcedomain.StatusCommand) (resourcedomain.Resource, error) {
if s.repository == nil {
return resourcedomain.Resource{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
}
command.AppCode = appcode.Normalize(command.AppCode)
ctx = appcode.WithContext(ctx, command.AppCode)
return s.repository.DeleteResource(ctx, command)
}
func (s *Service) BatchDeleteResources(ctx context.Context, command resourcedomain.BatchDeleteResourcesCommand) ([]resourcedomain.Resource, error) {
if s.repository == nil {
return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
}
command.AppCode = appcode.Normalize(command.AppCode)
ctx = appcode.WithContext(ctx, command.AppCode)
return s.repository.BatchDeleteResources(ctx, command)
}
func (s *Service) ListResourceGroups(ctx context.Context, query resourcedomain.ListResourceGroupsQuery) ([]resourcedomain.ResourceGroup, int64, error) {
if s.repository == nil {
return nil, 0, xerr.New(xerr.Unavailable, "wallet repository is not configured")
@ -145,6 +163,26 @@ func (s *Service) CreateGiftConfig(ctx context.Context, command resourcedomain.G
return s.repository.CreateGiftConfig(ctx, command)
}
func (s *Service) BatchCreateGiftConfigs(ctx context.Context, appCode string, commands []resourcedomain.GiftConfigCommand, operatorUserID int64) ([]resourcedomain.GiftConfig, error) {
if s.repository == nil {
return nil, xerr.New(xerr.Unavailable, "wallet repository is not configured")
}
appCode = appcode.Normalize(appCode)
ctx = appcode.WithContext(ctx, appCode)
if len(commands) == 0 {
return nil, xerr.New(xerr.InvalidArgument, "gift configs are required")
}
batchCommands := make([]resourcedomain.GiftConfigCommand, 0, len(commands))
for _, command := range commands {
command.AppCode = appCode
if command.OperatorUserID == 0 {
command.OperatorUserID = operatorUserID
}
batchCommands = append(batchCommands, command)
}
return s.repository.BatchCreateGiftConfigs(ctx, batchCommands)
}
func (s *Service) UpdateGiftConfig(ctx context.Context, command resourcedomain.GiftConfigCommand) (resourcedomain.GiftConfig, error) {
if s.repository == nil {
return resourcedomain.GiftConfig{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")

View File

@ -3694,6 +3694,195 @@ func TestDeleteGiftConfigKeepsResourceData(t *testing.T) {
}
}
func TestDeleteResourceHidesFromDefaultList(t *testing.T) {
repository := mysqltest.NewRepository(t)
svc := walletservice.New(repository)
ctx := context.Background()
resource, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
ResourceCode: "delete_resource_hidden",
ResourceType: resourcedomain.TypeBadge,
Name: "Delete Resource Hidden",
Status: resourcedomain.StatusActive,
Grantable: true,
GrantStrategy: resourcedomain.GrantStrategyNewEntitlement,
UsageScopes: []string{"badge"},
OperatorUserID: 91001,
})
if err != nil {
t.Fatalf("create resource failed: %v", err)
}
deleted, err := svc.DeleteResource(ctx, resourcedomain.StatusCommand{
ID: resource.ResourceID,
OperatorUserID: 91002,
})
if err != nil {
t.Fatalf("delete resource failed: %v", err)
}
if deleted.ResourceID != resource.ResourceID || deleted.Status != resourcedomain.StatusDeleted {
t.Fatalf("deleted resource snapshot mismatch: %+v", deleted)
}
if got := repository.CountRows("resources", "resource_id = ? AND status = ?", resource.ResourceID, resourcedomain.StatusDeleted); got != 1 {
t.Fatalf("resource should be soft deleted, got %d", got)
}
if got := repository.CountRows("resources", "resource_code = ?", "delete_resource_hidden"); got != 0 {
t.Fatalf("deleted resource should release resource_code uniqueness, got %d", got)
}
items, total, err := svc.ListResources(ctx, resourcedomain.ListResourcesQuery{
Keyword: "delete_resource_hidden",
Page: 1,
PageSize: 10,
})
if err != nil {
t.Fatalf("list resources failed: %v", err)
}
if total != 0 || len(items) != 0 {
t.Fatalf("deleted resource should be hidden from default list: total=%d items=%+v", total, items)
}
items, total, err = svc.ListResources(ctx, resourcedomain.ListResourcesQuery{
Keyword: "delete_resource_hidden",
Status: resourcedomain.StatusDeleted,
Page: 1,
PageSize: 10,
})
if err != nil {
t.Fatalf("list deleted status resources failed: %v", err)
}
if total != 0 || len(items) != 0 {
t.Fatalf("deleted resource should not be exposed by status filter: total=%d items=%+v", total, items)
}
loaded, err := svc.GetResource(ctx, "", resource.ResourceID)
if err != nil {
t.Fatalf("deleted resource should remain readable for historical references: %v", err)
}
if loaded.Status != resourcedomain.StatusDeleted {
t.Fatalf("loaded deleted resource status mismatch: %+v", loaded)
}
recreated, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
ResourceCode: "delete_resource_hidden",
ResourceType: resourcedomain.TypeBadge,
Name: "Recreated Resource",
Status: resourcedomain.StatusActive,
Grantable: true,
GrantStrategy: resourcedomain.GrantStrategyNewEntitlement,
UsageScopes: []string{"badge"},
OperatorUserID: 91003,
})
if err != nil {
t.Fatalf("recreate resource with deleted code failed: %v", err)
}
if recreated.ResourceID == resource.ResourceID {
t.Fatalf("recreated resource should be a new row: old=%d new=%d", resource.ResourceID, recreated.ResourceID)
}
}
func TestDeleteGiftResourceHidesGiftConfigList(t *testing.T) {
repository := mysqltest.NewRepository(t)
svc := walletservice.New(repository)
ctx := context.Background()
resource, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
ResourceCode: "delete_gift_resource_hidden",
ResourceType: resourcedomain.TypeGift,
Name: "Delete Gift Resource Hidden",
Status: resourcedomain.StatusActive,
Grantable: true,
GrantStrategy: resourcedomain.GrantStrategyIncreaseQuantity,
UsageScopes: []string{"gift"},
OperatorUserID: 91101,
})
if err != nil {
t.Fatalf("create gift resource failed: %v", err)
}
if _, err := svc.CreateGiftConfig(ctx, resourcedomain.GiftConfigCommand{
GiftID: "delete-resource-hide-gift",
ResourceID: resource.ResourceID,
Status: resourcedomain.StatusActive,
Name: "Delete Resource Hide Gift",
PriceVersion: "default",
CoinPrice: 10,
HeatValue: 10,
OperatorUserID: 91101,
RegionIDs: []int64{0},
}); err != nil {
t.Fatalf("create gift config failed: %v", err)
}
if _, err := svc.DeleteResource(ctx, resourcedomain.StatusCommand{
ID: resource.ResourceID,
OperatorUserID: 91102,
}); err != nil {
t.Fatalf("delete gift resource failed: %v", err)
}
gifts, total, err := svc.ListGiftConfigs(ctx, resourcedomain.ListGiftConfigsQuery{
Keyword: "delete-resource-hide-gift",
Page: 1,
PageSize: 10,
})
if err != nil {
t.Fatalf("list gift configs failed: %v", err)
}
if total != 0 || len(gifts) != 0 {
t.Fatalf("gift config should be hidden when resource is deleted: total=%d gifts=%+v", total, gifts)
}
if got := repository.CountRows("gift_configs", "gift_id = ?", "delete-resource-hide-gift"); got != 1 {
t.Fatalf("soft-deleting resource should keep gift config row for history, got %d", got)
}
}
func TestBatchCreateGiftConfigsRollsBackOnFailure(t *testing.T) {
repository := mysqltest.NewRepository(t)
svc := walletservice.New(repository)
ctx := context.Background()
resource, err := svc.CreateResource(ctx, resourcedomain.ResourceCommand{
ResourceCode: "batch_gift_valid_resource",
ResourceType: resourcedomain.TypeGift,
Name: "Batch Gift Valid Resource",
Status: resourcedomain.StatusActive,
Grantable: true,
GrantStrategy: resourcedomain.GrantStrategyIncreaseQuantity,
UsageScopes: []string{"gift"},
OperatorUserID: 91201,
})
if err != nil {
t.Fatalf("create gift resource failed: %v", err)
}
_, err = svc.BatchCreateGiftConfigs(ctx, "", []resourcedomain.GiftConfigCommand{
{
GiftID: "batch-create-rollback-ok",
ResourceID: resource.ResourceID,
Status: resourcedomain.StatusActive,
Name: "Batch Create Rollback OK",
PriceVersion: "default",
CoinPrice: 10,
HeatValue: 10,
OperatorUserID: 91201,
RegionIDs: []int64{0},
},
{
GiftID: "batch-create-rollback-bad",
ResourceID: 99999999,
Status: resourcedomain.StatusActive,
Name: "Batch Create Rollback Bad",
PriceVersion: "default",
CoinPrice: 10,
HeatValue: 10,
OperatorUserID: 91201,
RegionIDs: []int64{0},
},
}, 91201)
if err == nil {
t.Fatalf("batch create should fail when any item is invalid")
}
if got := repository.CountRows("gift_configs", "gift_id IN (?, ?)", "batch-create-rollback-ok", "batch-create-rollback-bad"); got != 0 {
t.Fatalf("batch create should roll back all gift rows, got %d", got)
}
if got := repository.CountRows("wallet_gift_prices", "gift_id IN (?, ?)", "batch-create-rollback-ok", "batch-create-rollback-bad"); got != 0 {
t.Fatalf("batch create should roll back all gift prices, got %d", got)
}
}
func TestGiftConfigSyncsConfiguredResourcePrice(t *testing.T) {
repository := mysqltest.NewRepository(t)
svc := walletservice.New(repository)

View File

@ -305,7 +305,7 @@ func giftConfigSelectSQL() string {
}
func giftConfigWhereSQL(query resourcedomain.ListGiftConfigsQuery) (string, []any) {
where := `WHERE gc.app_code = ?`
where := `WHERE gc.app_code = ? AND r.status <> 'deleted'`
args := []any{query.AppCode}
if query.ActiveOnly {
where += ` AND gc.status = 'active' AND r.status = 'active' AND r.resource_type = 'gift'

View File

@ -2,6 +2,7 @@ package mysql
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"hyapp/pkg/appcode"
@ -15,6 +16,41 @@ func (r *Repository) CreateGiftConfig(ctx context.Context, command resourcedomai
return r.upsertGiftConfig(ctx, command, false)
}
func (r *Repository) BatchCreateGiftConfigs(ctx context.Context, commands []resourcedomain.GiftConfigCommand) ([]resourcedomain.GiftConfig, error) {
if r == nil || r.db == nil {
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
if len(commands) == 0 {
return nil, xerr.New(xerr.InvalidArgument, "gift configs are required")
}
ctx = contextWithCommandApp(ctx, commands[0].AppCode)
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer func() { _ = tx.Rollback() }()
nowMs := time.Now().UnixMilli()
gifts := make([]resourcedomain.GiftConfig, 0, len(commands))
events := make([]walletOutboxEvent, 0, len(commands))
for _, command := range commands {
command.AppCode = appcode.FromContext(ctx)
gift, err := r.createGiftConfigTx(ctx, tx, command, nowMs)
if err != nil {
return nil, err
}
gifts = append(gifts, gift)
events = append(events, resourceOutboxEvent("GiftConfigChanged", fmt.Sprintf("gift:%s:config:%d", command.GiftID, nowMs), 0, command.ResourceID, command, nowMs))
}
if err := r.insertWalletOutbox(ctx, tx, events); err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
return gifts, nil
}
func (r *Repository) UpdateGiftConfig(ctx context.Context, command resourcedomain.GiftConfigCommand) (resourcedomain.GiftConfig, error) {
return r.upsertGiftConfig(ctx, command, true)
}
@ -118,52 +154,22 @@ func (r *Repository) upsertGiftConfig(ctx context.Context, command resourcedomai
}
defer func() { _ = tx.Rollback() }()
resource, err := r.getResourceForUpdate(ctx, tx, command.ResourceID)
if err != nil {
return resourcedomain.GiftConfig{}, err
}
if resource.ResourceType != resourcedomain.TypeGift {
return resourcedomain.GiftConfig{}, xerr.New(xerr.InvalidArgument, "gift config must select gift resource")
}
command = applyResourcePricingToGiftCommand(command, resource)
if resourcedomain.NormalizePriceType(resource.PriceType) == "" && command.CoinPrice <= 0 {
return resourcedomain.GiftConfig{}, xerr.New(xerr.InvalidArgument, "gift price is invalid")
}
if err := validateGiftConfigCommand(command); err != nil {
return resourcedomain.GiftConfig{}, err
}
if command.Status == resourcedomain.StatusActive && resource.Status != resourcedomain.StatusActive {
return resourcedomain.GiftConfig{}, xerr.New(xerr.Conflict, "gift resource is disabled")
}
if err := r.ensureDefaultGiftTypeConfigs(ctx, tx); err != nil {
return resourcedomain.GiftConfig{}, err
}
if ok, err := r.giftTypeConfigExistsTx(ctx, tx, command.GiftTypeCode); err != nil {
return resourcedomain.GiftConfig{}, err
} else if !ok {
return resourcedomain.GiftConfig{}, xerr.New(xerr.InvalidArgument, "gift type is not configured")
}
nowMs := time.Now().UnixMilli()
presentation := giftPresentationWithCPRelationType(command.PresentationJSON, command.CPRelationType)
effectTypesJSON, err := json.Marshal(command.EffectTypes)
if err != nil {
return resourcedomain.GiftConfig{}, err
}
var gift resourcedomain.GiftConfig
if !update {
if _, err := tx.ExecContext(ctx, `
INSERT INTO gift_configs (
app_code, gift_id, resource_id, status, name, sort_order, presentation_json,
gift_type_code, effective_from_ms, effective_to_ms, effect_types_json,
created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
command.AppCode, command.GiftID, command.ResourceID, command.Status, command.Name, command.SortOrder, presentation,
command.GiftTypeCode, command.EffectiveFromMS, command.EffectiveToMS, string(effectTypesJSON),
command.OperatorUserID, command.OperatorUserID, nowMs, nowMs,
); err != nil {
gift, err = r.createGiftConfigTx(ctx, tx, command, nowMs)
if err != nil {
return resourcedomain.GiftConfig{}, err
}
} else {
if err := r.prepareGiftConfigCommandTx(ctx, tx, &command); err != nil {
return resourcedomain.GiftConfig{}, err
}
presentation := giftPresentationWithCPRelationType(command.PresentationJSON, command.CPRelationType)
effectTypesJSON, err := json.Marshal(command.EffectTypes)
if err != nil {
return resourcedomain.GiftConfig{}, err
}
result, err := tx.ExecContext(ctx, `
UPDATE gift_configs
SET resource_id = ?, status = ?, name = ?, sort_order = ?, presentation_json = ?,
@ -182,12 +188,12 @@ func (r *Repository) upsertGiftConfig(ctx context.Context, command resourcedomai
} else if affected == 0 {
return resourcedomain.GiftConfig{}, xerr.New(xerr.NotFound, "gift config not found")
}
}
if err := r.upsertGiftPriceTx(ctx, tx, command, nowMs); err != nil {
return resourcedomain.GiftConfig{}, err
}
if err := r.replaceGiftConfigRegionsTx(ctx, tx, command.GiftID, command.RegionIDs, nowMs); err != nil {
return resourcedomain.GiftConfig{}, err
if err := r.upsertGiftPriceTx(ctx, tx, command, nowMs); err != nil {
return resourcedomain.GiftConfig{}, err
}
if err := r.replaceGiftConfigRegionsTx(ctx, tx, command.GiftID, command.RegionIDs, nowMs); err != nil {
return resourcedomain.GiftConfig{}, err
}
}
if err := r.insertWalletOutbox(ctx, tx, []walletOutboxEvent{
resourceOutboxEvent("GiftConfigChanged", fmt.Sprintf("gift:%s:config:%d", command.GiftID, nowMs), 0, command.ResourceID, command, nowMs),
@ -197,5 +203,67 @@ func (r *Repository) upsertGiftConfig(ctx context.Context, command resourcedomai
if err := tx.Commit(); err != nil {
return resourcedomain.GiftConfig{}, err
}
return r.getGiftConfig(ctx, command.GiftID)
if update {
return r.getGiftConfig(ctx, command.GiftID)
}
return gift, nil
}
func (r *Repository) createGiftConfigTx(ctx context.Context, tx *sql.Tx, command resourcedomain.GiftConfigCommand, nowMs int64) (resourcedomain.GiftConfig, error) {
if err := r.prepareGiftConfigCommandTx(ctx, tx, &command); err != nil {
return resourcedomain.GiftConfig{}, err
}
presentation := giftPresentationWithCPRelationType(command.PresentationJSON, command.CPRelationType)
effectTypesJSON, err := json.Marshal(command.EffectTypes)
if err != nil {
return resourcedomain.GiftConfig{}, err
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO gift_configs (
app_code, gift_id, resource_id, status, name, sort_order, presentation_json,
gift_type_code, effective_from_ms, effective_to_ms, effect_types_json,
created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
command.AppCode, command.GiftID, command.ResourceID, command.Status, command.Name, command.SortOrder, presentation,
command.GiftTypeCode, command.EffectiveFromMS, command.EffectiveToMS, string(effectTypesJSON),
command.OperatorUserID, command.OperatorUserID, nowMs, nowMs,
); err != nil {
return resourcedomain.GiftConfig{}, err
}
if err := r.upsertGiftPriceTx(ctx, tx, command, nowMs); err != nil {
return resourcedomain.GiftConfig{}, err
}
if err := r.replaceGiftConfigRegionsTx(ctx, tx, command.GiftID, command.RegionIDs, nowMs); err != nil {
return resourcedomain.GiftConfig{}, err
}
return r.getGiftConfigForUpdateTx(ctx, tx, command.GiftID)
}
func (r *Repository) prepareGiftConfigCommandTx(ctx context.Context, tx *sql.Tx, command *resourcedomain.GiftConfigCommand) error {
resource, err := r.getResourceForUpdate(ctx, tx, command.ResourceID)
if err != nil {
return err
}
if resource.ResourceType != resourcedomain.TypeGift {
return xerr.New(xerr.InvalidArgument, "gift config must select gift resource")
}
*command = applyResourcePricingToGiftCommand(*command, resource)
if resourcedomain.NormalizePriceType(resource.PriceType) == "" && command.CoinPrice <= 0 {
return xerr.New(xerr.InvalidArgument, "gift price is invalid")
}
if err := validateGiftConfigCommand(*command); err != nil {
return err
}
if command.Status == resourcedomain.StatusActive && resource.Status != resourcedomain.StatusActive {
return xerr.New(xerr.Conflict, "gift resource is disabled")
}
if err := r.ensureDefaultGiftTypeConfigs(ctx, tx); err != nil {
return err
}
if ok, err := r.giftTypeConfigExistsTx(ctx, tx, command.GiftTypeCode); err != nil {
return err
} else if !ok {
return xerr.New(xerr.InvalidArgument, "gift type is not configured")
}
return nil
}

View File

@ -342,6 +342,75 @@ func (r *Repository) SetResourceStatus(ctx context.Context, command resourcedoma
return resource, nil
}
func (r *Repository) DeleteResource(ctx context.Context, command resourcedomain.StatusCommand) (resourcedomain.Resource, error) {
resources, err := r.BatchDeleteResources(ctx, resourcedomain.BatchDeleteResourcesCommand{
AppCode: command.AppCode,
ResourceIDs: []int64{command.ID},
OperatorUserID: command.OperatorUserID,
})
if err != nil {
return resourcedomain.Resource{}, err
}
if len(resources) == 0 {
return resourcedomain.Resource{}, xerr.New(xerr.NotFound, "resource not found")
}
return resources[0], nil
}
func (r *Repository) BatchDeleteResources(ctx context.Context, command resourcedomain.BatchDeleteResourcesCommand) ([]resourcedomain.Resource, error) {
if r == nil || r.db == nil {
return nil, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
ctx = contextWithCommandApp(ctx, command.AppCode)
resourceIDs := normalizeResourceDeleteIDs(command.ResourceIDs)
if len(resourceIDs) == 0 {
return nil, xerr.New(xerr.InvalidArgument, "resource_ids are required")
}
nowMs := time.Now().UnixMilli()
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return nil, err
}
defer func() { _ = tx.Rollback() }()
deletedResources := make([]resourcedomain.Resource, 0, len(resourceIDs))
events := make([]walletOutboxEvent, 0, len(resourceIDs))
for _, resourceID := range resourceIDs {
resource, err := r.getResourceForUpdate(ctx, tx, resourceID)
if err != nil {
return nil, err
}
if resource.Status == resourcedomain.StatusDeleted {
return nil, xerr.New(xerr.NotFound, "resource not found")
}
result, err := tx.ExecContext(ctx,
`UPDATE resources SET resource_code = ?, status = ?, updated_by_user_id = ?, updated_at_ms = ? WHERE app_code = ? AND resource_id = ? AND status <> ?`,
deletedResourceCode(resource.ResourceCode, resourceID), resourcedomain.StatusDeleted, command.OperatorUserID, nowMs, appcode.FromContext(ctx), resourceID, resourcedomain.StatusDeleted,
)
if err != nil {
return nil, err
}
if affected, err := result.RowsAffected(); err != nil {
return nil, err
} else if affected == 0 {
return nil, xerr.New(xerr.NotFound, "resource not found")
}
resource.Status = resourcedomain.StatusDeleted
resource.UpdatedByUserID = command.OperatorUserID
resource.UpdatedAtMS = nowMs
deletedResources = append(deletedResources, resource)
events = append(events, resourceOutboxEvent("ResourceChanged", fmt.Sprintf("resource:%d:delete:%d", resourceID, nowMs), 0, resourceID, resource, nowMs))
}
if err := r.insertWalletOutbox(ctx, tx, events); err != nil {
return nil, err
}
if err := tx.Commit(); err != nil {
return nil, err
}
return deletedResources, nil
}
func (r *Repository) getResourceForUpdate(ctx context.Context, tx *sql.Tx, resourceID int64) (resourcedomain.Resource, error) {
row := tx.QueryRowContext(ctx, resourceSelectSQL()+` WHERE app_code = ? AND resource_id = ? FOR UPDATE`, appcode.FromContext(ctx), resourceID)
resource, err := scanResource(row)
@ -418,7 +487,7 @@ func nullableResourceColumnsWithAlias(alias string) string {
}
func resourceWhereSQL(query resourcedomain.ListResourcesQuery) (string, []any) {
where := `WHERE app_code = ?`
where := `WHERE app_code = ? AND status <> 'deleted'`
args := []any{query.AppCode}
if query.ManagerGrantOnly {
where += ` AND status = 'active' AND grantable = TRUE AND manager_grant_enabled = TRUE`
@ -440,6 +509,35 @@ func resourceWhereSQL(query resourcedomain.ListResourcesQuery) (string, []any) {
return where, args
}
func normalizeResourceDeleteIDs(resourceIDs []int64) []int64 {
seen := make(map[int64]struct{}, len(resourceIDs))
normalized := make([]int64, 0, len(resourceIDs))
for _, resourceID := range resourceIDs {
if resourceID <= 0 {
continue
}
if _, ok := seen[resourceID]; ok {
continue
}
seen[resourceID] = struct{}{}
normalized = append(normalized, resourceID)
}
return normalized
}
func deletedResourceCode(resourceCode string, resourceID int64) string {
const maxResourceCodeLength = 96
suffix := fmt.Sprintf("__deleted_%d", resourceID)
if len(resourceCode)+len(suffix) <= maxResourceCodeLength {
return resourceCode + suffix
}
keepLength := maxResourceCodeLength - len(suffix)
if keepLength <= 0 {
return suffix[len(suffix)-maxResourceCodeLength:]
}
return resourceCode[:keepLength] + suffix
}
func normalizeResourceListQuery(query resourcedomain.ListResourcesQuery) resourcedomain.ListResourcesQuery {
query.ResourceType = resourcedomain.NormalizeResourceType(query.ResourceType)
if query.ResourceType != "" && !resourcedomain.ValidResourceType(query.ResourceType) {

View File

@ -65,6 +65,34 @@ func (s *Server) SetResourceStatus(ctx context.Context, req *walletv1.SetResourc
return &walletv1.ResourceResponse{Resource: resourceToProto(resource)}, nil
}
func (s *Server) DeleteResource(ctx context.Context, req *walletv1.DeleteResourceRequest) (*walletv1.ResourceResponse, error) {
resource, err := s.svc.DeleteResource(ctx, resourcedomain.StatusCommand{
AppCode: req.GetAppCode(),
ID: req.GetResourceId(),
OperatorUserID: req.GetOperatorUserId(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &walletv1.ResourceResponse{Resource: resourceToProto(resource)}, nil
}
func (s *Server) BatchDeleteResources(ctx context.Context, req *walletv1.BatchDeleteResourcesRequest) (*walletv1.BatchDeleteResourcesResponse, error) {
resources, err := s.svc.BatchDeleteResources(ctx, resourcedomain.BatchDeleteResourcesCommand{
AppCode: req.GetAppCode(),
ResourceIDs: req.GetResourceIds(),
OperatorUserID: req.GetOperatorUserId(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &walletv1.BatchDeleteResourcesResponse{Resources: make([]*walletv1.Resource, 0, len(resources))}
for _, resource := range resources {
resp.Resources = append(resp.Resources, resourceToProto(resource))
}
return resp, nil
}
func (s *Server) ListResourceGroups(ctx context.Context, req *walletv1.ListResourceGroupsRequest) (*walletv1.ListResourceGroupsResponse, error) {
items, total, err := s.svc.ListResourceGroups(ctx, resourcedomain.ListResourceGroupsQuery{
AppCode: req.GetAppCode(),
@ -175,6 +203,27 @@ func (s *Server) CreateGiftConfig(ctx context.Context, req *walletv1.CreateGiftC
return &walletv1.GiftConfigResponse{Gift: giftConfigToProto(gift)}, nil
}
func (s *Server) BatchCreateGiftConfigs(ctx context.Context, req *walletv1.BatchCreateGiftConfigsRequest) (*walletv1.BatchCreateGiftConfigsResponse, error) {
commands := make([]resourcedomain.GiftConfigCommand, 0, len(req.GetItems()))
for _, item := range req.GetItems() {
command := giftConfigCommandFromCreate(item)
command.AppCode = req.GetAppCode()
if command.OperatorUserID == 0 {
command.OperatorUserID = req.GetOperatorUserId()
}
commands = append(commands, command)
}
gifts, err := s.svc.BatchCreateGiftConfigs(ctx, req.GetAppCode(), commands, req.GetOperatorUserId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
resp := &walletv1.BatchCreateGiftConfigsResponse{Gifts: make([]*walletv1.GiftConfig, 0, len(gifts))}
for _, gift := range gifts {
resp.Gifts = append(resp.Gifts, giftConfigToProto(gift))
}
return resp, nil
}
func (s *Server) UpdateGiftConfig(ctx context.Context, req *walletv1.UpdateGiftConfigRequest) (*walletv1.GiftConfigResponse, error) {
gift, err := s.svc.UpdateGiftConfig(ctx, giftConfigCommandFromUpdate(req))
if err != nil {