Compare commits

...

2 Commits

Author SHA1 Message Date
zhx
880f151feb feat: add voice room red packet APIs 2026-05-29 17:36:33 +08:00
zhx
7433b24411 feat: add user report management 2026-05-29 16:48:00 +08:00
48 changed files with 3351 additions and 503 deletions

File diff suppressed because it is too large Load Diff

View File

@ -360,6 +360,35 @@ message ListFriendApplicationsResponse {
int64 total = 2;
}
// UserReport App
message UserReport {
string report_id = 1;
int64 reporter_user_id = 2;
string target_type = 3;
int64 user_id = 4;
string room_id = 5;
string report_type = 6;
string reason = 7;
repeated string image_urls = 8;
string status = 9;
int64 created_at_ms = 10;
}
// SubmitReportRequest user_id room_id
message SubmitReportRequest {
RequestMeta meta = 1;
int64 reporter_user_id = 2;
int64 user_id = 3;
string room_id = 4;
string report_type = 5;
string reason = 6;
repeated string image_urls = 7;
}
message SubmitReportResponse {
UserReport report = 1;
}
// BatchGetUsersRequest
message BatchGetUsersRequest {
RequestMeta meta = 1;
@ -716,6 +745,7 @@ service UserSocialService {
rpc DeleteFriend(DeleteFriendRequest) returns (DeleteFriendResponse);
rpc ListFriends(ListFriendsRequest) returns (ListFriendsResponse);
rpc ListFriendApplications(ListFriendApplicationsRequest) returns (ListFriendApplicationsResponse);
rpc SubmitReport(SubmitReportRequest) returns (SubmitReportResponse);
}
// UserCronService cron-service user-service owner

View File

@ -477,6 +477,7 @@ const (
UserSocialService_DeleteFriend_FullMethodName = "/hyapp.user.v1.UserSocialService/DeleteFriend"
UserSocialService_ListFriends_FullMethodName = "/hyapp.user.v1.UserSocialService/ListFriends"
UserSocialService_ListFriendApplications_FullMethodName = "/hyapp.user.v1.UserSocialService/ListFriendApplications"
UserSocialService_SubmitReport_FullMethodName = "/hyapp.user.v1.UserSocialService/SubmitReport"
)
// UserSocialServiceClient is the client API for UserSocialService service.
@ -495,6 +496,7 @@ type UserSocialServiceClient interface {
DeleteFriend(ctx context.Context, in *DeleteFriendRequest, opts ...grpc.CallOption) (*DeleteFriendResponse, error)
ListFriends(ctx context.Context, in *ListFriendsRequest, opts ...grpc.CallOption) (*ListFriendsResponse, error)
ListFriendApplications(ctx context.Context, in *ListFriendApplicationsRequest, opts ...grpc.CallOption) (*ListFriendApplicationsResponse, error)
SubmitReport(ctx context.Context, in *SubmitReportRequest, opts ...grpc.CallOption) (*SubmitReportResponse, error)
}
type userSocialServiceClient struct {
@ -605,6 +607,16 @@ func (c *userSocialServiceClient) ListFriendApplications(ctx context.Context, in
return out, nil
}
func (c *userSocialServiceClient) SubmitReport(ctx context.Context, in *SubmitReportRequest, opts ...grpc.CallOption) (*SubmitReportResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(SubmitReportResponse)
err := c.cc.Invoke(ctx, UserSocialService_SubmitReport_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// UserSocialServiceServer is the server API for UserSocialService service.
// All implementations must embed UnimplementedUserSocialServiceServer
// for forward compatibility.
@ -621,6 +633,7 @@ type UserSocialServiceServer interface {
DeleteFriend(context.Context, *DeleteFriendRequest) (*DeleteFriendResponse, error)
ListFriends(context.Context, *ListFriendsRequest) (*ListFriendsResponse, error)
ListFriendApplications(context.Context, *ListFriendApplicationsRequest) (*ListFriendApplicationsResponse, error)
SubmitReport(context.Context, *SubmitReportRequest) (*SubmitReportResponse, error)
mustEmbedUnimplementedUserSocialServiceServer()
}
@ -661,6 +674,9 @@ func (UnimplementedUserSocialServiceServer) ListFriends(context.Context, *ListFr
func (UnimplementedUserSocialServiceServer) ListFriendApplications(context.Context, *ListFriendApplicationsRequest) (*ListFriendApplicationsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ListFriendApplications not implemented")
}
func (UnimplementedUserSocialServiceServer) SubmitReport(context.Context, *SubmitReportRequest) (*SubmitReportResponse, error) {
return nil, status.Error(codes.Unimplemented, "method SubmitReport not implemented")
}
func (UnimplementedUserSocialServiceServer) mustEmbedUnimplementedUserSocialServiceServer() {}
func (UnimplementedUserSocialServiceServer) testEmbeddedByValue() {}
@ -862,6 +878,24 @@ func _UserSocialService_ListFriendApplications_Handler(srv interface{}, ctx cont
return interceptor(ctx, in, info, handler)
}
func _UserSocialService_SubmitReport_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(SubmitReportRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(UserSocialServiceServer).SubmitReport(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: UserSocialService_SubmitReport_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(UserSocialServiceServer).SubmitReport(ctx, req.(*SubmitReportRequest))
}
return interceptor(ctx, in, info, handler)
}
// UserSocialService_ServiceDesc is the grpc.ServiceDesc for UserSocialService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -909,6 +943,10 @@ var UserSocialService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ListFriendApplications",
Handler: _UserSocialService_ListFriendApplications_Handler,
},
{
MethodName: "SubmitReport",
Handler: _UserSocialService_SubmitReport_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/user/v1/user.proto",

View File

@ -10676,6 +10676,7 @@ type RedPacketConfig struct {
UpdatedByAdminId int64 `protobuf:"varint,8,opt,name=updated_by_admin_id,json=updatedByAdminId,proto3" json:"updated_by_admin_id,omitempty"`
CreatedAtMs int64 `protobuf:"varint,9,opt,name=created_at_ms,json=createdAtMs,proto3" json:"created_at_ms,omitempty"`
UpdatedAtMs int64 `protobuf:"varint,10,opt,name=updated_at_ms,json=updatedAtMs,proto3" json:"updated_at_ms,omitempty"`
RuleUrl string `protobuf:"bytes,11,opt,name=rule_url,json=ruleUrl,proto3" json:"rule_url,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -10780,6 +10781,13 @@ func (x *RedPacketConfig) GetUpdatedAtMs() int64 {
return 0
}
func (x *RedPacketConfig) GetRuleUrl() string {
if x != nil {
return x.RuleUrl
}
return ""
}
type RedPacketClaim struct {
state protoimpl.MessageState `protogen:"open.v1"`
AppCode string `protobuf:"bytes,1,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
@ -10925,6 +10933,9 @@ type RedPacket struct {
ClaimedByViewer bool `protobuf:"varint,17,opt,name=claimed_by_viewer,json=claimedByViewer,proto3" json:"claimed_by_viewer,omitempty"`
ViewerClaimAmount int64 `protobuf:"varint,18,opt,name=viewer_claim_amount,json=viewerClaimAmount,proto3" json:"viewer_claim_amount,omitempty"`
Claims []*RedPacketClaim `protobuf:"bytes,19,rep,name=claims,proto3" json:"claims,omitempty"`
RefundAmount int64 `protobuf:"varint,20,opt,name=refund_amount,json=refundAmount,proto3" json:"refund_amount,omitempty"`
RefundStatus string `protobuf:"bytes,21,opt,name=refund_status,json=refundStatus,proto3" json:"refund_status,omitempty"`
RefundedAtMs int64 `protobuf:"varint,22,opt,name=refunded_at_ms,json=refundedAtMs,proto3" json:"refunded_at_ms,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -11092,6 +11103,27 @@ func (x *RedPacket) GetClaims() []*RedPacketClaim {
return nil
}
func (x *RedPacket) GetRefundAmount() int64 {
if x != nil {
return x.RefundAmount
}
return 0
}
func (x *RedPacket) GetRefundStatus() string {
if x != nil {
return x.RefundStatus
}
return ""
}
func (x *RedPacket) GetRefundedAtMs() int64 {
if x != nil {
return x.RefundedAtMs
}
return 0
}
type GetRedPacketConfigRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
@ -11207,6 +11239,7 @@ type UpdateRedPacketConfigRequest struct {
ExpireSeconds int32 `protobuf:"varint,7,opt,name=expire_seconds,json=expireSeconds,proto3" json:"expire_seconds,omitempty"`
DailySendLimit int32 `protobuf:"varint,8,opt,name=daily_send_limit,json=dailySendLimit,proto3" json:"daily_send_limit,omitempty"`
OperatorUserId int64 `protobuf:"varint,9,opt,name=operator_user_id,json=operatorUserId,proto3" json:"operator_user_id,omitempty"`
RuleUrl string `protobuf:"bytes,10,opt,name=rule_url,json=ruleUrl,proto3" json:"rule_url,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -11304,6 +11337,13 @@ func (x *UpdateRedPacketConfigRequest) GetOperatorUserId() int64 {
return 0
}
func (x *UpdateRedPacketConfigRequest) GetRuleUrl() string {
if x != nil {
return x.RuleUrl
}
return ""
}
type UpdateRedPacketConfigResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Config *RedPacketConfig `protobuf:"bytes,1,opt,name=config,proto3" json:"config,omitempty"`
@ -11605,6 +11645,7 @@ type ClaimRedPacketResponse struct {
Claim *RedPacketClaim `protobuf:"bytes,1,opt,name=claim,proto3" json:"claim,omitempty"`
BalanceAfter int64 `protobuf:"varint,2,opt,name=balance_after,json=balanceAfter,proto3" json:"balance_after,omitempty"`
ServerTimeMs int64 `protobuf:"varint,3,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
Packet *RedPacket `protobuf:"bytes,4,opt,name=packet,proto3" json:"packet,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@ -11660,6 +11701,13 @@ func (x *ClaimRedPacketResponse) GetServerTimeMs() int64 {
return 0
}
func (x *ClaimRedPacketResponse) GetPacket() *RedPacket {
if x != nil {
return x.Packet
}
return nil
}
type ListRedPacketsRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
@ -12100,6 +12148,134 @@ func (x *ExpireRedPacketsResponse) GetServerTimeMs() int64 {
return 0
}
type RetryRedPacketRefundRequest struct {
state protoimpl.MessageState `protogen:"open.v1"`
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
AppCode string `protobuf:"bytes,2,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"`
PacketId string `protobuf:"bytes,3,opt,name=packet_id,json=packetId,proto3" json:"packet_id,omitempty"`
OperatorUserId int64 `protobuf:"varint,4,opt,name=operator_user_id,json=operatorUserId,proto3" json:"operator_user_id,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RetryRedPacketRefundRequest) Reset() {
*x = RetryRedPacketRefundRequest{}
mi := &file_proto_wallet_v1_wallet_proto_msgTypes[137]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RetryRedPacketRefundRequest) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RetryRedPacketRefundRequest) ProtoMessage() {}
func (x *RetryRedPacketRefundRequest) ProtoReflect() protoreflect.Message {
mi := &file_proto_wallet_v1_wallet_proto_msgTypes[137]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RetryRedPacketRefundRequest.ProtoReflect.Descriptor instead.
func (*RetryRedPacketRefundRequest) Descriptor() ([]byte, []int) {
return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{137}
}
func (x *RetryRedPacketRefundRequest) GetRequestId() string {
if x != nil {
return x.RequestId
}
return ""
}
func (x *RetryRedPacketRefundRequest) GetAppCode() string {
if x != nil {
return x.AppCode
}
return ""
}
func (x *RetryRedPacketRefundRequest) GetPacketId() string {
if x != nil {
return x.PacketId
}
return ""
}
func (x *RetryRedPacketRefundRequest) GetOperatorUserId() int64 {
if x != nil {
return x.OperatorUserId
}
return 0
}
type RetryRedPacketRefundResponse struct {
state protoimpl.MessageState `protogen:"open.v1"`
Packet *RedPacket `protobuf:"bytes,1,opt,name=packet,proto3" json:"packet,omitempty"`
RefundedAmount int64 `protobuf:"varint,2,opt,name=refunded_amount,json=refundedAmount,proto3" json:"refunded_amount,omitempty"`
ServerTimeMs int64 `protobuf:"varint,3,opt,name=server_time_ms,json=serverTimeMs,proto3" json:"server_time_ms,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *RetryRedPacketRefundResponse) Reset() {
*x = RetryRedPacketRefundResponse{}
mi := &file_proto_wallet_v1_wallet_proto_msgTypes[138]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *RetryRedPacketRefundResponse) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*RetryRedPacketRefundResponse) ProtoMessage() {}
func (x *RetryRedPacketRefundResponse) ProtoReflect() protoreflect.Message {
mi := &file_proto_wallet_v1_wallet_proto_msgTypes[138]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use RetryRedPacketRefundResponse.ProtoReflect.Descriptor instead.
func (*RetryRedPacketRefundResponse) Descriptor() ([]byte, []int) {
return file_proto_wallet_v1_wallet_proto_rawDescGZIP(), []int{138}
}
func (x *RetryRedPacketRefundResponse) GetPacket() *RedPacket {
if x != nil {
return x.Packet
}
return nil
}
func (x *RetryRedPacketRefundResponse) GetRefundedAmount() int64 {
if x != nil {
return x.RefundedAmount
}
return 0
}
func (x *RetryRedPacketRefundResponse) GetServerTimeMs() int64 {
if x != nil {
return x.ServerTimeMs
}
return 0
}
var File_proto_wallet_v1_wallet_proto protoreflect.FileDescriptor
const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
@ -13214,7 +13390,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
"\x1bApplyGameCoinChangeResponse\x122\n" +
"\x15wallet_transaction_id\x18\x01 \x01(\tR\x13walletTransactionId\x12#\n" +
"\rbalance_after\x18\x02 \x01(\x03R\fbalanceAfter\x12+\n" +
"\x11idempotent_replay\x18\x03 \x01(\bR\x10idempotentReplay\"\x84\x03\n" +
"\x11idempotent_replay\x18\x03 \x01(\bR\x10idempotentReplay\"\x9f\x03\n" +
"\x0fRedPacketConfig\x12\x19\n" +
"\bapp_code\x18\x01 \x01(\tR\aappCode\x12\x18\n" +
"\aenabled\x18\x02 \x01(\bR\aenabled\x12\x1f\n" +
@ -13227,7 +13403,8 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
"\x13updated_by_admin_id\x18\b \x01(\x03R\x10updatedByAdminId\x12\"\n" +
"\rcreated_at_ms\x18\t \x01(\x03R\vcreatedAtMs\x12\"\n" +
"\rupdated_at_ms\x18\n" +
" \x01(\x03R\vupdatedAtMs\"\xee\x02\n" +
" \x01(\x03R\vupdatedAtMs\x12\x19\n" +
"\brule_url\x18\v \x01(\tR\aruleUrl\"\xee\x02\n" +
"\x0eRedPacketClaim\x12\x19\n" +
"\bapp_code\x18\x01 \x01(\tR\aappCode\x12\x19\n" +
"\bclaim_id\x18\x02 \x01(\tR\aclaimId\x12\x1d\n" +
@ -13241,7 +13418,7 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
"\x0efailure_reason\x18\t \x01(\tR\rfailureReason\x12\"\n" +
"\rcreated_at_ms\x18\n" +
" \x01(\x03R\vcreatedAtMs\x12\"\n" +
"\rupdated_at_ms\x18\v \x01(\x03R\vupdatedAtMs\"\xb0\x05\n" +
"\rupdated_at_ms\x18\v \x01(\x03R\vupdatedAtMs\"\xa0\x06\n" +
"\tRedPacket\x12\x19\n" +
"\bapp_code\x18\x01 \x01(\tR\aappCode\x12\x1b\n" +
"\tpacket_id\x18\x02 \x01(\tR\bpacketId\x12\x1d\n" +
@ -13265,14 +13442,17 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
"\rupdated_at_ms\x18\x10 \x01(\x03R\vupdatedAtMs\x12*\n" +
"\x11claimed_by_viewer\x18\x11 \x01(\bR\x0fclaimedByViewer\x12.\n" +
"\x13viewer_claim_amount\x18\x12 \x01(\x03R\x11viewerClaimAmount\x127\n" +
"\x06claims\x18\x13 \x03(\v2\x1f.hyapp.wallet.v1.RedPacketClaimR\x06claims\"U\n" +
"\x06claims\x18\x13 \x03(\v2\x1f.hyapp.wallet.v1.RedPacketClaimR\x06claims\x12#\n" +
"\rrefund_amount\x18\x14 \x01(\x03R\frefundAmount\x12#\n" +
"\rrefund_status\x18\x15 \x01(\tR\frefundStatus\x12$\n" +
"\x0erefunded_at_ms\x18\x16 \x01(\x03R\frefundedAtMs\"U\n" +
"\x19GetRedPacketConfigRequest\x12\x1d\n" +
"\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" +
"\bapp_code\x18\x02 \x01(\tR\aappCode\"|\n" +
"\x1aGetRedPacketConfigResponse\x128\n" +
"\x06config\x18\x01 \x01(\v2 .hyapp.wallet.v1.RedPacketConfigR\x06config\x12$\n" +
"\x0eserver_time_ms\x18\x02 \x01(\x03R\fserverTimeMs\"\xe3\x02\n" +
"\x0eserver_time_ms\x18\x02 \x01(\x03R\fserverTimeMs\"\xfe\x02\n" +
"\x1cUpdateRedPacketConfigRequest\x12\x1d\n" +
"\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" +
@ -13284,7 +13464,9 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
"\x14delayed_open_seconds\x18\x06 \x01(\x05R\x12delayedOpenSeconds\x12%\n" +
"\x0eexpire_seconds\x18\a \x01(\x05R\rexpireSeconds\x12(\n" +
"\x10daily_send_limit\x18\b \x01(\x05R\x0edailySendLimit\x12(\n" +
"\x10operator_user_id\x18\t \x01(\x03R\x0eoperatorUserId\"\x7f\n" +
"\x10operator_user_id\x18\t \x01(\x03R\x0eoperatorUserId\x12\x19\n" +
"\brule_url\x18\n" +
" \x01(\tR\aruleUrl\"\x7f\n" +
"\x1dUpdateRedPacketConfigResponse\x128\n" +
"\x06config\x18\x01 \x01(\v2 .hyapp.wallet.v1.RedPacketConfigR\x06config\x12$\n" +
"\x0eserver_time_ms\x18\x02 \x01(\x03R\fserverTimeMs\"\xb4\x02\n" +
@ -13312,11 +13494,12 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
"\n" +
"command_id\x18\x03 \x01(\tR\tcommandId\x12\x1b\n" +
"\tpacket_id\x18\x04 \x01(\tR\bpacketId\x12\x17\n" +
"\auser_id\x18\x05 \x01(\x03R\x06userId\"\x9a\x01\n" +
"\auser_id\x18\x05 \x01(\x03R\x06userId\"\xce\x01\n" +
"\x16ClaimRedPacketResponse\x125\n" +
"\x05claim\x18\x01 \x01(\v2\x1f.hyapp.wallet.v1.RedPacketClaimR\x05claim\x12#\n" +
"\rbalance_after\x18\x02 \x01(\x03R\fbalanceAfter\x12$\n" +
"\x0eserver_time_ms\x18\x03 \x01(\x03R\fserverTimeMs\"\xf9\x02\n" +
"\x0eserver_time_ms\x18\x03 \x01(\x03R\fserverTimeMs\x122\n" +
"\x06packet\x18\x04 \x01(\v2\x1a.hyapp.wallet.v1.RedPacketR\x06packet\"\xf9\x02\n" +
"\x15ListRedPacketsRequest\x12\x1d\n" +
"\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" +
@ -13356,7 +13539,17 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
"\x18ExpireRedPacketsResponse\x12#\n" +
"\rexpired_count\x18\x01 \x01(\x05R\fexpiredCount\x12'\n" +
"\x0frefunded_amount\x18\x02 \x01(\x03R\x0erefundedAmount\x12$\n" +
"\x0eserver_time_ms\x18\x03 \x01(\x03R\fserverTimeMs2\xf01\n" +
"\x0eserver_time_ms\x18\x03 \x01(\x03R\fserverTimeMs\"\x9e\x01\n" +
"\x1bRetryRedPacketRefundRequest\x12\x1d\n" +
"\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x12\x19\n" +
"\bapp_code\x18\x02 \x01(\tR\aappCode\x12\x1b\n" +
"\tpacket_id\x18\x03 \x01(\tR\bpacketId\x12(\n" +
"\x10operator_user_id\x18\x04 \x01(\x03R\x0eoperatorUserId\"\xa1\x01\n" +
"\x1cRetryRedPacketRefundResponse\x122\n" +
"\x06packet\x18\x01 \x01(\v2\x1a.hyapp.wallet.v1.RedPacketR\x06packet\x12'\n" +
"\x0frefunded_amount\x18\x02 \x01(\x03R\x0erefundedAmount\x12$\n" +
"\x0eserver_time_ms\x18\x03 \x01(\x03R\fserverTimeMs2\xe52\n" +
"\rWalletService\x12R\n" +
"\tDebitGift\x12!.hyapp.wallet.v1.DebitGiftRequest\x1a\".hyapp.wallet.v1.DebitGiftResponse\x12X\n" +
"\vGetBalances\x12#.hyapp.wallet.v1.GetBalancesRequest\x1a$.hyapp.wallet.v1.GetBalancesResponse\x12g\n" +
@ -13416,7 +13609,8 @@ const file_proto_wallet_v1_wallet_proto_rawDesc = "" +
"\x0eClaimRedPacket\x12&.hyapp.wallet.v1.ClaimRedPacketRequest\x1a'.hyapp.wallet.v1.ClaimRedPacketResponse\x12a\n" +
"\x0eListRedPackets\x12&.hyapp.wallet.v1.ListRedPacketsRequest\x1a'.hyapp.wallet.v1.ListRedPacketsResponse\x12[\n" +
"\fGetRedPacket\x12$.hyapp.wallet.v1.GetRedPacketRequest\x1a%.hyapp.wallet.v1.GetRedPacketResponse\x12g\n" +
"\x10ExpireRedPackets\x12(.hyapp.wallet.v1.ExpireRedPacketsRequest\x1a).hyapp.wallet.v1.ExpireRedPacketsResponseB*Z(hyapp.local/api/proto/wallet/v1;walletv1b\x06proto3"
"\x10ExpireRedPackets\x12(.hyapp.wallet.v1.ExpireRedPacketsRequest\x1a).hyapp.wallet.v1.ExpireRedPacketsResponse\x12s\n" +
"\x14RetryRedPacketRefund\x12,.hyapp.wallet.v1.RetryRedPacketRefundRequest\x1a-.hyapp.wallet.v1.RetryRedPacketRefundResponseB*Z(hyapp.local/api/proto/wallet/v1;walletv1b\x06proto3"
var (
file_proto_wallet_v1_wallet_proto_rawDescOnce sync.Once
@ -13430,7 +13624,7 @@ func file_proto_wallet_v1_wallet_proto_rawDescGZIP() []byte {
return file_proto_wallet_v1_wallet_proto_rawDescData
}
var file_proto_wallet_v1_wallet_proto_msgTypes = make([]protoimpl.MessageInfo, 137)
var file_proto_wallet_v1_wallet_proto_msgTypes = make([]protoimpl.MessageInfo, 139)
var file_proto_wallet_v1_wallet_proto_goTypes = []any{
(*DebitGiftRequest)(nil), // 0: hyapp.wallet.v1.DebitGiftRequest
(*DebitGiftResponse)(nil), // 1: hyapp.wallet.v1.DebitGiftResponse
@ -13569,6 +13763,8 @@ var file_proto_wallet_v1_wallet_proto_goTypes = []any{
(*GetRedPacketResponse)(nil), // 134: hyapp.wallet.v1.GetRedPacketResponse
(*ExpireRedPacketsRequest)(nil), // 135: hyapp.wallet.v1.ExpireRedPacketsRequest
(*ExpireRedPacketsResponse)(nil), // 136: hyapp.wallet.v1.ExpireRedPacketsResponse
(*RetryRedPacketRefundRequest)(nil), // 137: hyapp.wallet.v1.RetryRedPacketRefundRequest
(*RetryRedPacketRefundResponse)(nil), // 138: hyapp.wallet.v1.RetryRedPacketRefundResponse
}
var file_proto_wallet_v1_wallet_proto_depIdxs = []int32{
2, // 0: hyapp.wallet.v1.GetBalancesResponse.balances:type_name -> hyapp.wallet.v1.AssetBalance
@ -13634,131 +13830,135 @@ var file_proto_wallet_v1_wallet_proto_depIdxs = []int32{
120, // 60: hyapp.wallet.v1.UpdateRedPacketConfigResponse.config:type_name -> hyapp.wallet.v1.RedPacketConfig
122, // 61: hyapp.wallet.v1.CreateRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket
121, // 62: hyapp.wallet.v1.ClaimRedPacketResponse.claim:type_name -> hyapp.wallet.v1.RedPacketClaim
122, // 63: hyapp.wallet.v1.ListRedPacketsResponse.packets:type_name -> hyapp.wallet.v1.RedPacket
122, // 64: hyapp.wallet.v1.GetRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket
0, // 65: hyapp.wallet.v1.WalletService.DebitGift:input_type -> hyapp.wallet.v1.DebitGiftRequest
3, // 66: hyapp.wallet.v1.WalletService.GetBalances:input_type -> hyapp.wallet.v1.GetBalancesRequest
5, // 67: hyapp.wallet.v1.WalletService.AdminCreditAsset:input_type -> hyapp.wallet.v1.AdminCreditAssetRequest
7, // 68: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:input_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockRequest
9, // 69: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:input_type -> hyapp.wallet.v1.TransferCoinFromSellerRequest
21, // 70: hyapp.wallet.v1.WalletService.ListResources:input_type -> hyapp.wallet.v1.ListResourcesRequest
23, // 71: hyapp.wallet.v1.WalletService.GetResource:input_type -> hyapp.wallet.v1.GetResourceRequest
25, // 72: hyapp.wallet.v1.WalletService.CreateResource:input_type -> hyapp.wallet.v1.CreateResourceRequest
26, // 73: hyapp.wallet.v1.WalletService.UpdateResource:input_type -> hyapp.wallet.v1.UpdateResourceRequest
27, // 74: hyapp.wallet.v1.WalletService.SetResourceStatus:input_type -> hyapp.wallet.v1.SetResourceStatusRequest
29, // 75: hyapp.wallet.v1.WalletService.ListResourceGroups:input_type -> hyapp.wallet.v1.ListResourceGroupsRequest
31, // 76: hyapp.wallet.v1.WalletService.GetResourceGroup:input_type -> hyapp.wallet.v1.GetResourceGroupRequest
33, // 77: hyapp.wallet.v1.WalletService.CreateResourceGroup:input_type -> hyapp.wallet.v1.CreateResourceGroupRequest
34, // 78: hyapp.wallet.v1.WalletService.UpdateResourceGroup:input_type -> hyapp.wallet.v1.UpdateResourceGroupRequest
35, // 79: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:input_type -> hyapp.wallet.v1.SetResourceGroupStatusRequest
37, // 80: hyapp.wallet.v1.WalletService.ListGiftConfigs:input_type -> hyapp.wallet.v1.ListGiftConfigsRequest
39, // 81: hyapp.wallet.v1.WalletService.ListGiftTypeConfigs:input_type -> hyapp.wallet.v1.ListGiftTypeConfigsRequest
43, // 82: hyapp.wallet.v1.WalletService.CreateGiftConfig:input_type -> hyapp.wallet.v1.CreateGiftConfigRequest
44, // 83: hyapp.wallet.v1.WalletService.UpdateGiftConfig:input_type -> hyapp.wallet.v1.UpdateGiftConfigRequest
45, // 84: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:input_type -> hyapp.wallet.v1.SetGiftConfigStatusRequest
41, // 85: hyapp.wallet.v1.WalletService.UpsertGiftTypeConfig:input_type -> hyapp.wallet.v1.UpsertGiftTypeConfigRequest
47, // 86: hyapp.wallet.v1.WalletService.GrantResource:input_type -> hyapp.wallet.v1.GrantResourceRequest
48, // 87: hyapp.wallet.v1.WalletService.GrantResourceGroup:input_type -> hyapp.wallet.v1.GrantResourceGroupRequest
50, // 88: hyapp.wallet.v1.WalletService.ListUserResources:input_type -> hyapp.wallet.v1.ListUserResourcesRequest
52, // 89: hyapp.wallet.v1.WalletService.EquipUserResource:input_type -> hyapp.wallet.v1.EquipUserResourceRequest
54, // 90: hyapp.wallet.v1.WalletService.ListResourceGrants:input_type -> hyapp.wallet.v1.ListResourceGrantsRequest
57, // 91: hyapp.wallet.v1.WalletService.ListResourceShopItems:input_type -> hyapp.wallet.v1.ListResourceShopItemsRequest
59, // 92: hyapp.wallet.v1.WalletService.UpsertResourceShopItems:input_type -> hyapp.wallet.v1.UpsertResourceShopItemsRequest
61, // 93: hyapp.wallet.v1.WalletService.SetResourceShopItemStatus:input_type -> hyapp.wallet.v1.SetResourceShopItemStatusRequest
63, // 94: hyapp.wallet.v1.WalletService.PurchaseResourceShopItem:input_type -> hyapp.wallet.v1.PurchaseResourceShopItemRequest
66, // 95: hyapp.wallet.v1.WalletService.ListRechargeBills:input_type -> hyapp.wallet.v1.ListRechargeBillsRequest
69, // 96: hyapp.wallet.v1.WalletService.GetWalletOverview:input_type -> hyapp.wallet.v1.GetWalletOverviewRequest
72, // 97: hyapp.wallet.v1.WalletService.GetWalletValueSummary:input_type -> hyapp.wallet.v1.GetWalletValueSummaryRequest
75, // 98: hyapp.wallet.v1.WalletService.GetUserGiftWall:input_type -> hyapp.wallet.v1.GetUserGiftWallRequest
78, // 99: hyapp.wallet.v1.WalletService.ListRechargeProducts:input_type -> hyapp.wallet.v1.ListRechargeProductsRequest
80, // 100: hyapp.wallet.v1.WalletService.ConfirmGooglePayment:input_type -> hyapp.wallet.v1.ConfirmGooglePaymentRequest
82, // 101: hyapp.wallet.v1.WalletService.ListAdminRechargeProducts:input_type -> hyapp.wallet.v1.ListAdminRechargeProductsRequest
84, // 102: hyapp.wallet.v1.WalletService.CreateRechargeProduct:input_type -> hyapp.wallet.v1.CreateRechargeProductRequest
85, // 103: hyapp.wallet.v1.WalletService.UpdateRechargeProduct:input_type -> hyapp.wallet.v1.UpdateRechargeProductRequest
86, // 104: hyapp.wallet.v1.WalletService.DeleteRechargeProduct:input_type -> hyapp.wallet.v1.DeleteRechargeProductRequest
90, // 105: hyapp.wallet.v1.WalletService.GetDiamondExchangeConfig:input_type -> hyapp.wallet.v1.GetDiamondExchangeConfigRequest
93, // 106: hyapp.wallet.v1.WalletService.ListWalletTransactions:input_type -> hyapp.wallet.v1.ListWalletTransactionsRequest
96, // 107: hyapp.wallet.v1.WalletService.ApplyWithdrawal:input_type -> hyapp.wallet.v1.ApplyWithdrawalRequest
101, // 108: hyapp.wallet.v1.WalletService.ListVipPackages:input_type -> hyapp.wallet.v1.ListVipPackagesRequest
103, // 109: hyapp.wallet.v1.WalletService.GetMyVip:input_type -> hyapp.wallet.v1.GetMyVipRequest
105, // 110: hyapp.wallet.v1.WalletService.PurchaseVip:input_type -> hyapp.wallet.v1.PurchaseVipRequest
107, // 111: hyapp.wallet.v1.WalletService.GrantVip:input_type -> hyapp.wallet.v1.GrantVipRequest
110, // 112: hyapp.wallet.v1.WalletService.ListAdminVipLevels:input_type -> hyapp.wallet.v1.ListAdminVipLevelsRequest
112, // 113: hyapp.wallet.v1.WalletService.UpdateAdminVipLevels:input_type -> hyapp.wallet.v1.UpdateAdminVipLevelsRequest
114, // 114: hyapp.wallet.v1.WalletService.CreditTaskReward:input_type -> hyapp.wallet.v1.CreditTaskRewardRequest
116, // 115: hyapp.wallet.v1.WalletService.CreditLuckyGiftReward:input_type -> hyapp.wallet.v1.CreditLuckyGiftRewardRequest
118, // 116: hyapp.wallet.v1.WalletService.ApplyGameCoinChange:input_type -> hyapp.wallet.v1.ApplyGameCoinChangeRequest
123, // 117: hyapp.wallet.v1.WalletService.GetRedPacketConfig:input_type -> hyapp.wallet.v1.GetRedPacketConfigRequest
125, // 118: hyapp.wallet.v1.WalletService.UpdateRedPacketConfig:input_type -> hyapp.wallet.v1.UpdateRedPacketConfigRequest
127, // 119: hyapp.wallet.v1.WalletService.CreateRedPacket:input_type -> hyapp.wallet.v1.CreateRedPacketRequest
129, // 120: hyapp.wallet.v1.WalletService.ClaimRedPacket:input_type -> hyapp.wallet.v1.ClaimRedPacketRequest
131, // 121: hyapp.wallet.v1.WalletService.ListRedPackets:input_type -> hyapp.wallet.v1.ListRedPacketsRequest
133, // 122: hyapp.wallet.v1.WalletService.GetRedPacket:input_type -> hyapp.wallet.v1.GetRedPacketRequest
135, // 123: hyapp.wallet.v1.WalletService.ExpireRedPackets:input_type -> hyapp.wallet.v1.ExpireRedPacketsRequest
1, // 124: hyapp.wallet.v1.WalletService.DebitGift:output_type -> hyapp.wallet.v1.DebitGiftResponse
4, // 125: hyapp.wallet.v1.WalletService.GetBalances:output_type -> hyapp.wallet.v1.GetBalancesResponse
6, // 126: hyapp.wallet.v1.WalletService.AdminCreditAsset:output_type -> hyapp.wallet.v1.AdminCreditAssetResponse
8, // 127: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:output_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockResponse
10, // 128: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:output_type -> hyapp.wallet.v1.TransferCoinFromSellerResponse
22, // 129: hyapp.wallet.v1.WalletService.ListResources:output_type -> hyapp.wallet.v1.ListResourcesResponse
24, // 130: hyapp.wallet.v1.WalletService.GetResource:output_type -> hyapp.wallet.v1.GetResourceResponse
28, // 131: hyapp.wallet.v1.WalletService.CreateResource:output_type -> hyapp.wallet.v1.ResourceResponse
28, // 132: hyapp.wallet.v1.WalletService.UpdateResource:output_type -> hyapp.wallet.v1.ResourceResponse
28, // 133: hyapp.wallet.v1.WalletService.SetResourceStatus:output_type -> hyapp.wallet.v1.ResourceResponse
30, // 134: hyapp.wallet.v1.WalletService.ListResourceGroups:output_type -> hyapp.wallet.v1.ListResourceGroupsResponse
32, // 135: hyapp.wallet.v1.WalletService.GetResourceGroup:output_type -> hyapp.wallet.v1.GetResourceGroupResponse
36, // 136: hyapp.wallet.v1.WalletService.CreateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse
36, // 137: hyapp.wallet.v1.WalletService.UpdateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse
36, // 138: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:output_type -> hyapp.wallet.v1.ResourceGroupResponse
38, // 139: hyapp.wallet.v1.WalletService.ListGiftConfigs:output_type -> hyapp.wallet.v1.ListGiftConfigsResponse
40, // 140: hyapp.wallet.v1.WalletService.ListGiftTypeConfigs:output_type -> hyapp.wallet.v1.ListGiftTypeConfigsResponse
46, // 141: hyapp.wallet.v1.WalletService.CreateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse
46, // 142: hyapp.wallet.v1.WalletService.UpdateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse
46, // 143: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:output_type -> hyapp.wallet.v1.GiftConfigResponse
42, // 144: hyapp.wallet.v1.WalletService.UpsertGiftTypeConfig:output_type -> hyapp.wallet.v1.GiftTypeConfigResponse
49, // 145: hyapp.wallet.v1.WalletService.GrantResource:output_type -> hyapp.wallet.v1.ResourceGrantResponse
49, // 146: hyapp.wallet.v1.WalletService.GrantResourceGroup:output_type -> hyapp.wallet.v1.ResourceGrantResponse
51, // 147: hyapp.wallet.v1.WalletService.ListUserResources:output_type -> hyapp.wallet.v1.ListUserResourcesResponse
53, // 148: hyapp.wallet.v1.WalletService.EquipUserResource:output_type -> hyapp.wallet.v1.EquipUserResourceResponse
55, // 149: hyapp.wallet.v1.WalletService.ListResourceGrants:output_type -> hyapp.wallet.v1.ListResourceGrantsResponse
58, // 150: hyapp.wallet.v1.WalletService.ListResourceShopItems:output_type -> hyapp.wallet.v1.ListResourceShopItemsResponse
60, // 151: hyapp.wallet.v1.WalletService.UpsertResourceShopItems:output_type -> hyapp.wallet.v1.UpsertResourceShopItemsResponse
62, // 152: hyapp.wallet.v1.WalletService.SetResourceShopItemStatus:output_type -> hyapp.wallet.v1.ResourceShopItemResponse
64, // 153: hyapp.wallet.v1.WalletService.PurchaseResourceShopItem:output_type -> hyapp.wallet.v1.PurchaseResourceShopItemResponse
67, // 154: hyapp.wallet.v1.WalletService.ListRechargeBills:output_type -> hyapp.wallet.v1.ListRechargeBillsResponse
70, // 155: hyapp.wallet.v1.WalletService.GetWalletOverview:output_type -> hyapp.wallet.v1.GetWalletOverviewResponse
73, // 156: hyapp.wallet.v1.WalletService.GetWalletValueSummary:output_type -> hyapp.wallet.v1.GetWalletValueSummaryResponse
76, // 157: hyapp.wallet.v1.WalletService.GetUserGiftWall:output_type -> hyapp.wallet.v1.GetUserGiftWallResponse
79, // 158: hyapp.wallet.v1.WalletService.ListRechargeProducts:output_type -> hyapp.wallet.v1.ListRechargeProductsResponse
81, // 159: hyapp.wallet.v1.WalletService.ConfirmGooglePayment:output_type -> hyapp.wallet.v1.ConfirmGooglePaymentResponse
83, // 160: hyapp.wallet.v1.WalletService.ListAdminRechargeProducts:output_type -> hyapp.wallet.v1.ListAdminRechargeProductsResponse
87, // 161: hyapp.wallet.v1.WalletService.CreateRechargeProduct:output_type -> hyapp.wallet.v1.RechargeProductResponse
87, // 162: hyapp.wallet.v1.WalletService.UpdateRechargeProduct:output_type -> hyapp.wallet.v1.RechargeProductResponse
88, // 163: hyapp.wallet.v1.WalletService.DeleteRechargeProduct:output_type -> hyapp.wallet.v1.DeleteRechargeProductResponse
91, // 164: hyapp.wallet.v1.WalletService.GetDiamondExchangeConfig:output_type -> hyapp.wallet.v1.GetDiamondExchangeConfigResponse
94, // 165: hyapp.wallet.v1.WalletService.ListWalletTransactions:output_type -> hyapp.wallet.v1.ListWalletTransactionsResponse
97, // 166: hyapp.wallet.v1.WalletService.ApplyWithdrawal:output_type -> hyapp.wallet.v1.ApplyWithdrawalResponse
102, // 167: hyapp.wallet.v1.WalletService.ListVipPackages:output_type -> hyapp.wallet.v1.ListVipPackagesResponse
104, // 168: hyapp.wallet.v1.WalletService.GetMyVip:output_type -> hyapp.wallet.v1.GetMyVipResponse
106, // 169: hyapp.wallet.v1.WalletService.PurchaseVip:output_type -> hyapp.wallet.v1.PurchaseVipResponse
108, // 170: hyapp.wallet.v1.WalletService.GrantVip:output_type -> hyapp.wallet.v1.GrantVipResponse
111, // 171: hyapp.wallet.v1.WalletService.ListAdminVipLevels:output_type -> hyapp.wallet.v1.ListAdminVipLevelsResponse
113, // 172: hyapp.wallet.v1.WalletService.UpdateAdminVipLevels:output_type -> hyapp.wallet.v1.UpdateAdminVipLevelsResponse
115, // 173: hyapp.wallet.v1.WalletService.CreditTaskReward:output_type -> hyapp.wallet.v1.CreditTaskRewardResponse
117, // 174: hyapp.wallet.v1.WalletService.CreditLuckyGiftReward:output_type -> hyapp.wallet.v1.CreditLuckyGiftRewardResponse
119, // 175: hyapp.wallet.v1.WalletService.ApplyGameCoinChange:output_type -> hyapp.wallet.v1.ApplyGameCoinChangeResponse
124, // 176: hyapp.wallet.v1.WalletService.GetRedPacketConfig:output_type -> hyapp.wallet.v1.GetRedPacketConfigResponse
126, // 177: hyapp.wallet.v1.WalletService.UpdateRedPacketConfig:output_type -> hyapp.wallet.v1.UpdateRedPacketConfigResponse
128, // 178: hyapp.wallet.v1.WalletService.CreateRedPacket:output_type -> hyapp.wallet.v1.CreateRedPacketResponse
130, // 179: hyapp.wallet.v1.WalletService.ClaimRedPacket:output_type -> hyapp.wallet.v1.ClaimRedPacketResponse
132, // 180: hyapp.wallet.v1.WalletService.ListRedPackets:output_type -> hyapp.wallet.v1.ListRedPacketsResponse
134, // 181: hyapp.wallet.v1.WalletService.GetRedPacket:output_type -> hyapp.wallet.v1.GetRedPacketResponse
136, // 182: hyapp.wallet.v1.WalletService.ExpireRedPackets:output_type -> hyapp.wallet.v1.ExpireRedPacketsResponse
124, // [124:183] is the sub-list for method output_type
65, // [65:124] is the sub-list for method input_type
65, // [65:65] is the sub-list for extension type_name
65, // [65:65] is the sub-list for extension extendee
0, // [0:65] is the sub-list for field type_name
122, // 63: hyapp.wallet.v1.ClaimRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket
122, // 64: hyapp.wallet.v1.ListRedPacketsResponse.packets:type_name -> hyapp.wallet.v1.RedPacket
122, // 65: hyapp.wallet.v1.GetRedPacketResponse.packet:type_name -> hyapp.wallet.v1.RedPacket
122, // 66: hyapp.wallet.v1.RetryRedPacketRefundResponse.packet:type_name -> hyapp.wallet.v1.RedPacket
0, // 67: hyapp.wallet.v1.WalletService.DebitGift:input_type -> hyapp.wallet.v1.DebitGiftRequest
3, // 68: hyapp.wallet.v1.WalletService.GetBalances:input_type -> hyapp.wallet.v1.GetBalancesRequest
5, // 69: hyapp.wallet.v1.WalletService.AdminCreditAsset:input_type -> hyapp.wallet.v1.AdminCreditAssetRequest
7, // 70: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:input_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockRequest
9, // 71: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:input_type -> hyapp.wallet.v1.TransferCoinFromSellerRequest
21, // 72: hyapp.wallet.v1.WalletService.ListResources:input_type -> hyapp.wallet.v1.ListResourcesRequest
23, // 73: hyapp.wallet.v1.WalletService.GetResource:input_type -> hyapp.wallet.v1.GetResourceRequest
25, // 74: hyapp.wallet.v1.WalletService.CreateResource:input_type -> hyapp.wallet.v1.CreateResourceRequest
26, // 75: hyapp.wallet.v1.WalletService.UpdateResource:input_type -> hyapp.wallet.v1.UpdateResourceRequest
27, // 76: hyapp.wallet.v1.WalletService.SetResourceStatus:input_type -> hyapp.wallet.v1.SetResourceStatusRequest
29, // 77: hyapp.wallet.v1.WalletService.ListResourceGroups:input_type -> hyapp.wallet.v1.ListResourceGroupsRequest
31, // 78: hyapp.wallet.v1.WalletService.GetResourceGroup:input_type -> hyapp.wallet.v1.GetResourceGroupRequest
33, // 79: hyapp.wallet.v1.WalletService.CreateResourceGroup:input_type -> hyapp.wallet.v1.CreateResourceGroupRequest
34, // 80: hyapp.wallet.v1.WalletService.UpdateResourceGroup:input_type -> hyapp.wallet.v1.UpdateResourceGroupRequest
35, // 81: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:input_type -> hyapp.wallet.v1.SetResourceGroupStatusRequest
37, // 82: hyapp.wallet.v1.WalletService.ListGiftConfigs:input_type -> hyapp.wallet.v1.ListGiftConfigsRequest
39, // 83: hyapp.wallet.v1.WalletService.ListGiftTypeConfigs:input_type -> hyapp.wallet.v1.ListGiftTypeConfigsRequest
43, // 84: hyapp.wallet.v1.WalletService.CreateGiftConfig:input_type -> hyapp.wallet.v1.CreateGiftConfigRequest
44, // 85: hyapp.wallet.v1.WalletService.UpdateGiftConfig:input_type -> hyapp.wallet.v1.UpdateGiftConfigRequest
45, // 86: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:input_type -> hyapp.wallet.v1.SetGiftConfigStatusRequest
41, // 87: hyapp.wallet.v1.WalletService.UpsertGiftTypeConfig:input_type -> hyapp.wallet.v1.UpsertGiftTypeConfigRequest
47, // 88: hyapp.wallet.v1.WalletService.GrantResource:input_type -> hyapp.wallet.v1.GrantResourceRequest
48, // 89: hyapp.wallet.v1.WalletService.GrantResourceGroup:input_type -> hyapp.wallet.v1.GrantResourceGroupRequest
50, // 90: hyapp.wallet.v1.WalletService.ListUserResources:input_type -> hyapp.wallet.v1.ListUserResourcesRequest
52, // 91: hyapp.wallet.v1.WalletService.EquipUserResource:input_type -> hyapp.wallet.v1.EquipUserResourceRequest
54, // 92: hyapp.wallet.v1.WalletService.ListResourceGrants:input_type -> hyapp.wallet.v1.ListResourceGrantsRequest
57, // 93: hyapp.wallet.v1.WalletService.ListResourceShopItems:input_type -> hyapp.wallet.v1.ListResourceShopItemsRequest
59, // 94: hyapp.wallet.v1.WalletService.UpsertResourceShopItems:input_type -> hyapp.wallet.v1.UpsertResourceShopItemsRequest
61, // 95: hyapp.wallet.v1.WalletService.SetResourceShopItemStatus:input_type -> hyapp.wallet.v1.SetResourceShopItemStatusRequest
63, // 96: hyapp.wallet.v1.WalletService.PurchaseResourceShopItem:input_type -> hyapp.wallet.v1.PurchaseResourceShopItemRequest
66, // 97: hyapp.wallet.v1.WalletService.ListRechargeBills:input_type -> hyapp.wallet.v1.ListRechargeBillsRequest
69, // 98: hyapp.wallet.v1.WalletService.GetWalletOverview:input_type -> hyapp.wallet.v1.GetWalletOverviewRequest
72, // 99: hyapp.wallet.v1.WalletService.GetWalletValueSummary:input_type -> hyapp.wallet.v1.GetWalletValueSummaryRequest
75, // 100: hyapp.wallet.v1.WalletService.GetUserGiftWall:input_type -> hyapp.wallet.v1.GetUserGiftWallRequest
78, // 101: hyapp.wallet.v1.WalletService.ListRechargeProducts:input_type -> hyapp.wallet.v1.ListRechargeProductsRequest
80, // 102: hyapp.wallet.v1.WalletService.ConfirmGooglePayment:input_type -> hyapp.wallet.v1.ConfirmGooglePaymentRequest
82, // 103: hyapp.wallet.v1.WalletService.ListAdminRechargeProducts:input_type -> hyapp.wallet.v1.ListAdminRechargeProductsRequest
84, // 104: hyapp.wallet.v1.WalletService.CreateRechargeProduct:input_type -> hyapp.wallet.v1.CreateRechargeProductRequest
85, // 105: hyapp.wallet.v1.WalletService.UpdateRechargeProduct:input_type -> hyapp.wallet.v1.UpdateRechargeProductRequest
86, // 106: hyapp.wallet.v1.WalletService.DeleteRechargeProduct:input_type -> hyapp.wallet.v1.DeleteRechargeProductRequest
90, // 107: hyapp.wallet.v1.WalletService.GetDiamondExchangeConfig:input_type -> hyapp.wallet.v1.GetDiamondExchangeConfigRequest
93, // 108: hyapp.wallet.v1.WalletService.ListWalletTransactions:input_type -> hyapp.wallet.v1.ListWalletTransactionsRequest
96, // 109: hyapp.wallet.v1.WalletService.ApplyWithdrawal:input_type -> hyapp.wallet.v1.ApplyWithdrawalRequest
101, // 110: hyapp.wallet.v1.WalletService.ListVipPackages:input_type -> hyapp.wallet.v1.ListVipPackagesRequest
103, // 111: hyapp.wallet.v1.WalletService.GetMyVip:input_type -> hyapp.wallet.v1.GetMyVipRequest
105, // 112: hyapp.wallet.v1.WalletService.PurchaseVip:input_type -> hyapp.wallet.v1.PurchaseVipRequest
107, // 113: hyapp.wallet.v1.WalletService.GrantVip:input_type -> hyapp.wallet.v1.GrantVipRequest
110, // 114: hyapp.wallet.v1.WalletService.ListAdminVipLevels:input_type -> hyapp.wallet.v1.ListAdminVipLevelsRequest
112, // 115: hyapp.wallet.v1.WalletService.UpdateAdminVipLevels:input_type -> hyapp.wallet.v1.UpdateAdminVipLevelsRequest
114, // 116: hyapp.wallet.v1.WalletService.CreditTaskReward:input_type -> hyapp.wallet.v1.CreditTaskRewardRequest
116, // 117: hyapp.wallet.v1.WalletService.CreditLuckyGiftReward:input_type -> hyapp.wallet.v1.CreditLuckyGiftRewardRequest
118, // 118: hyapp.wallet.v1.WalletService.ApplyGameCoinChange:input_type -> hyapp.wallet.v1.ApplyGameCoinChangeRequest
123, // 119: hyapp.wallet.v1.WalletService.GetRedPacketConfig:input_type -> hyapp.wallet.v1.GetRedPacketConfigRequest
125, // 120: hyapp.wallet.v1.WalletService.UpdateRedPacketConfig:input_type -> hyapp.wallet.v1.UpdateRedPacketConfigRequest
127, // 121: hyapp.wallet.v1.WalletService.CreateRedPacket:input_type -> hyapp.wallet.v1.CreateRedPacketRequest
129, // 122: hyapp.wallet.v1.WalletService.ClaimRedPacket:input_type -> hyapp.wallet.v1.ClaimRedPacketRequest
131, // 123: hyapp.wallet.v1.WalletService.ListRedPackets:input_type -> hyapp.wallet.v1.ListRedPacketsRequest
133, // 124: hyapp.wallet.v1.WalletService.GetRedPacket:input_type -> hyapp.wallet.v1.GetRedPacketRequest
135, // 125: hyapp.wallet.v1.WalletService.ExpireRedPackets:input_type -> hyapp.wallet.v1.ExpireRedPacketsRequest
137, // 126: hyapp.wallet.v1.WalletService.RetryRedPacketRefund:input_type -> hyapp.wallet.v1.RetryRedPacketRefundRequest
1, // 127: hyapp.wallet.v1.WalletService.DebitGift:output_type -> hyapp.wallet.v1.DebitGiftResponse
4, // 128: hyapp.wallet.v1.WalletService.GetBalances:output_type -> hyapp.wallet.v1.GetBalancesResponse
6, // 129: hyapp.wallet.v1.WalletService.AdminCreditAsset:output_type -> hyapp.wallet.v1.AdminCreditAssetResponse
8, // 130: hyapp.wallet.v1.WalletService.AdminCreditCoinSellerStock:output_type -> hyapp.wallet.v1.AdminCreditCoinSellerStockResponse
10, // 131: hyapp.wallet.v1.WalletService.TransferCoinFromSeller:output_type -> hyapp.wallet.v1.TransferCoinFromSellerResponse
22, // 132: hyapp.wallet.v1.WalletService.ListResources:output_type -> hyapp.wallet.v1.ListResourcesResponse
24, // 133: hyapp.wallet.v1.WalletService.GetResource:output_type -> hyapp.wallet.v1.GetResourceResponse
28, // 134: hyapp.wallet.v1.WalletService.CreateResource:output_type -> hyapp.wallet.v1.ResourceResponse
28, // 135: hyapp.wallet.v1.WalletService.UpdateResource:output_type -> hyapp.wallet.v1.ResourceResponse
28, // 136: hyapp.wallet.v1.WalletService.SetResourceStatus:output_type -> hyapp.wallet.v1.ResourceResponse
30, // 137: hyapp.wallet.v1.WalletService.ListResourceGroups:output_type -> hyapp.wallet.v1.ListResourceGroupsResponse
32, // 138: hyapp.wallet.v1.WalletService.GetResourceGroup:output_type -> hyapp.wallet.v1.GetResourceGroupResponse
36, // 139: hyapp.wallet.v1.WalletService.CreateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse
36, // 140: hyapp.wallet.v1.WalletService.UpdateResourceGroup:output_type -> hyapp.wallet.v1.ResourceGroupResponse
36, // 141: hyapp.wallet.v1.WalletService.SetResourceGroupStatus:output_type -> hyapp.wallet.v1.ResourceGroupResponse
38, // 142: hyapp.wallet.v1.WalletService.ListGiftConfigs:output_type -> hyapp.wallet.v1.ListGiftConfigsResponse
40, // 143: hyapp.wallet.v1.WalletService.ListGiftTypeConfigs:output_type -> hyapp.wallet.v1.ListGiftTypeConfigsResponse
46, // 144: hyapp.wallet.v1.WalletService.CreateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse
46, // 145: hyapp.wallet.v1.WalletService.UpdateGiftConfig:output_type -> hyapp.wallet.v1.GiftConfigResponse
46, // 146: hyapp.wallet.v1.WalletService.SetGiftConfigStatus:output_type -> hyapp.wallet.v1.GiftConfigResponse
42, // 147: hyapp.wallet.v1.WalletService.UpsertGiftTypeConfig:output_type -> hyapp.wallet.v1.GiftTypeConfigResponse
49, // 148: hyapp.wallet.v1.WalletService.GrantResource:output_type -> hyapp.wallet.v1.ResourceGrantResponse
49, // 149: hyapp.wallet.v1.WalletService.GrantResourceGroup:output_type -> hyapp.wallet.v1.ResourceGrantResponse
51, // 150: hyapp.wallet.v1.WalletService.ListUserResources:output_type -> hyapp.wallet.v1.ListUserResourcesResponse
53, // 151: hyapp.wallet.v1.WalletService.EquipUserResource:output_type -> hyapp.wallet.v1.EquipUserResourceResponse
55, // 152: hyapp.wallet.v1.WalletService.ListResourceGrants:output_type -> hyapp.wallet.v1.ListResourceGrantsResponse
58, // 153: hyapp.wallet.v1.WalletService.ListResourceShopItems:output_type -> hyapp.wallet.v1.ListResourceShopItemsResponse
60, // 154: hyapp.wallet.v1.WalletService.UpsertResourceShopItems:output_type -> hyapp.wallet.v1.UpsertResourceShopItemsResponse
62, // 155: hyapp.wallet.v1.WalletService.SetResourceShopItemStatus:output_type -> hyapp.wallet.v1.ResourceShopItemResponse
64, // 156: hyapp.wallet.v1.WalletService.PurchaseResourceShopItem:output_type -> hyapp.wallet.v1.PurchaseResourceShopItemResponse
67, // 157: hyapp.wallet.v1.WalletService.ListRechargeBills:output_type -> hyapp.wallet.v1.ListRechargeBillsResponse
70, // 158: hyapp.wallet.v1.WalletService.GetWalletOverview:output_type -> hyapp.wallet.v1.GetWalletOverviewResponse
73, // 159: hyapp.wallet.v1.WalletService.GetWalletValueSummary:output_type -> hyapp.wallet.v1.GetWalletValueSummaryResponse
76, // 160: hyapp.wallet.v1.WalletService.GetUserGiftWall:output_type -> hyapp.wallet.v1.GetUserGiftWallResponse
79, // 161: hyapp.wallet.v1.WalletService.ListRechargeProducts:output_type -> hyapp.wallet.v1.ListRechargeProductsResponse
81, // 162: hyapp.wallet.v1.WalletService.ConfirmGooglePayment:output_type -> hyapp.wallet.v1.ConfirmGooglePaymentResponse
83, // 163: hyapp.wallet.v1.WalletService.ListAdminRechargeProducts:output_type -> hyapp.wallet.v1.ListAdminRechargeProductsResponse
87, // 164: hyapp.wallet.v1.WalletService.CreateRechargeProduct:output_type -> hyapp.wallet.v1.RechargeProductResponse
87, // 165: hyapp.wallet.v1.WalletService.UpdateRechargeProduct:output_type -> hyapp.wallet.v1.RechargeProductResponse
88, // 166: hyapp.wallet.v1.WalletService.DeleteRechargeProduct:output_type -> hyapp.wallet.v1.DeleteRechargeProductResponse
91, // 167: hyapp.wallet.v1.WalletService.GetDiamondExchangeConfig:output_type -> hyapp.wallet.v1.GetDiamondExchangeConfigResponse
94, // 168: hyapp.wallet.v1.WalletService.ListWalletTransactions:output_type -> hyapp.wallet.v1.ListWalletTransactionsResponse
97, // 169: hyapp.wallet.v1.WalletService.ApplyWithdrawal:output_type -> hyapp.wallet.v1.ApplyWithdrawalResponse
102, // 170: hyapp.wallet.v1.WalletService.ListVipPackages:output_type -> hyapp.wallet.v1.ListVipPackagesResponse
104, // 171: hyapp.wallet.v1.WalletService.GetMyVip:output_type -> hyapp.wallet.v1.GetMyVipResponse
106, // 172: hyapp.wallet.v1.WalletService.PurchaseVip:output_type -> hyapp.wallet.v1.PurchaseVipResponse
108, // 173: hyapp.wallet.v1.WalletService.GrantVip:output_type -> hyapp.wallet.v1.GrantVipResponse
111, // 174: hyapp.wallet.v1.WalletService.ListAdminVipLevels:output_type -> hyapp.wallet.v1.ListAdminVipLevelsResponse
113, // 175: hyapp.wallet.v1.WalletService.UpdateAdminVipLevels:output_type -> hyapp.wallet.v1.UpdateAdminVipLevelsResponse
115, // 176: hyapp.wallet.v1.WalletService.CreditTaskReward:output_type -> hyapp.wallet.v1.CreditTaskRewardResponse
117, // 177: hyapp.wallet.v1.WalletService.CreditLuckyGiftReward:output_type -> hyapp.wallet.v1.CreditLuckyGiftRewardResponse
119, // 178: hyapp.wallet.v1.WalletService.ApplyGameCoinChange:output_type -> hyapp.wallet.v1.ApplyGameCoinChangeResponse
124, // 179: hyapp.wallet.v1.WalletService.GetRedPacketConfig:output_type -> hyapp.wallet.v1.GetRedPacketConfigResponse
126, // 180: hyapp.wallet.v1.WalletService.UpdateRedPacketConfig:output_type -> hyapp.wallet.v1.UpdateRedPacketConfigResponse
128, // 181: hyapp.wallet.v1.WalletService.CreateRedPacket:output_type -> hyapp.wallet.v1.CreateRedPacketResponse
130, // 182: hyapp.wallet.v1.WalletService.ClaimRedPacket:output_type -> hyapp.wallet.v1.ClaimRedPacketResponse
132, // 183: hyapp.wallet.v1.WalletService.ListRedPackets:output_type -> hyapp.wallet.v1.ListRedPacketsResponse
134, // 184: hyapp.wallet.v1.WalletService.GetRedPacket:output_type -> hyapp.wallet.v1.GetRedPacketResponse
136, // 185: hyapp.wallet.v1.WalletService.ExpireRedPackets:output_type -> hyapp.wallet.v1.ExpireRedPacketsResponse
138, // 186: hyapp.wallet.v1.WalletService.RetryRedPacketRefund:output_type -> hyapp.wallet.v1.RetryRedPacketRefundResponse
127, // [127:187] is the sub-list for method output_type
67, // [67:127] is the sub-list for method input_type
67, // [67:67] is the sub-list for extension type_name
67, // [67:67] is the sub-list for extension extendee
0, // [0:67] is the sub-list for field type_name
}
func init() { file_proto_wallet_v1_wallet_proto_init() }
@ -13774,7 +13974,7 @@ func file_proto_wallet_v1_wallet_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_proto_wallet_v1_wallet_proto_rawDesc), len(file_proto_wallet_v1_wallet_proto_rawDesc)),
NumEnums: 0,
NumMessages: 137,
NumMessages: 139,
NumExtensions: 0,
NumServices: 1,
},

View File

@ -1189,6 +1189,7 @@ message RedPacketConfig {
int64 updated_by_admin_id = 8;
int64 created_at_ms = 9;
int64 updated_at_ms = 10;
string rule_url = 11;
}
message RedPacketClaim {
@ -1225,6 +1226,9 @@ message RedPacket {
bool claimed_by_viewer = 17;
int64 viewer_claim_amount = 18;
repeated RedPacketClaim claims = 19;
int64 refund_amount = 20;
string refund_status = 21;
int64 refunded_at_ms = 22;
}
message GetRedPacketConfigRequest {
@ -1247,6 +1251,7 @@ message UpdateRedPacketConfigRequest {
int32 expire_seconds = 7;
int32 daily_send_limit = 8;
int64 operator_user_id = 9;
string rule_url = 10;
}
message UpdateRedPacketConfigResponse {
@ -1284,6 +1289,7 @@ message ClaimRedPacketResponse {
RedPacketClaim claim = 1;
int64 balance_after = 2;
int64 server_time_ms = 3;
RedPacket packet = 4;
}
message ListRedPacketsRequest {
@ -1332,6 +1338,19 @@ message ExpireRedPacketsResponse {
int64 server_time_ms = 3;
}
message RetryRedPacketRefundRequest {
string request_id = 1;
string app_code = 2;
string packet_id = 3;
int64 operator_user_id = 4;
}
message RetryRedPacketRefundResponse {
RedPacket packet = 1;
int64 refunded_amount = 2;
int64 server_time_ms = 3;
}
// WalletService gRPC HTTP gateway-service
service WalletService {
rpc DebitGift(DebitGiftRequest) returns (DebitGiftResponse);
@ -1393,4 +1412,5 @@ service WalletService {
rpc ListRedPackets(ListRedPacketsRequest) returns (ListRedPacketsResponse);
rpc GetRedPacket(GetRedPacketRequest) returns (GetRedPacketResponse);
rpc ExpireRedPackets(ExpireRedPacketsRequest) returns (ExpireRedPacketsResponse);
rpc RetryRedPacketRefund(RetryRedPacketRefundRequest) returns (RetryRedPacketRefundResponse);
}

View File

@ -78,6 +78,7 @@ const (
WalletService_ListRedPackets_FullMethodName = "/hyapp.wallet.v1.WalletService/ListRedPackets"
WalletService_GetRedPacket_FullMethodName = "/hyapp.wallet.v1.WalletService/GetRedPacket"
WalletService_ExpireRedPackets_FullMethodName = "/hyapp.wallet.v1.WalletService/ExpireRedPackets"
WalletService_RetryRedPacketRefund_FullMethodName = "/hyapp.wallet.v1.WalletService/RetryRedPacketRefund"
)
// WalletServiceClient is the client API for WalletService service.
@ -145,6 +146,7 @@ type WalletServiceClient interface {
ListRedPackets(ctx context.Context, in *ListRedPacketsRequest, opts ...grpc.CallOption) (*ListRedPacketsResponse, error)
GetRedPacket(ctx context.Context, in *GetRedPacketRequest, opts ...grpc.CallOption) (*GetRedPacketResponse, error)
ExpireRedPackets(ctx context.Context, in *ExpireRedPacketsRequest, opts ...grpc.CallOption) (*ExpireRedPacketsResponse, error)
RetryRedPacketRefund(ctx context.Context, in *RetryRedPacketRefundRequest, opts ...grpc.CallOption) (*RetryRedPacketRefundResponse, error)
}
type walletServiceClient struct {
@ -745,6 +747,16 @@ func (c *walletServiceClient) ExpireRedPackets(ctx context.Context, in *ExpireRe
return out, nil
}
func (c *walletServiceClient) RetryRedPacketRefund(ctx context.Context, in *RetryRedPacketRefundRequest, opts ...grpc.CallOption) (*RetryRedPacketRefundResponse, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(RetryRedPacketRefundResponse)
err := c.cc.Invoke(ctx, WalletService_RetryRedPacketRefund_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// WalletServiceServer is the server API for WalletService service.
// All implementations must embed UnimplementedWalletServiceServer
// for forward compatibility.
@ -810,6 +822,7 @@ type WalletServiceServer interface {
ListRedPackets(context.Context, *ListRedPacketsRequest) (*ListRedPacketsResponse, error)
GetRedPacket(context.Context, *GetRedPacketRequest) (*GetRedPacketResponse, error)
ExpireRedPackets(context.Context, *ExpireRedPacketsRequest) (*ExpireRedPacketsResponse, error)
RetryRedPacketRefund(context.Context, *RetryRedPacketRefundRequest) (*RetryRedPacketRefundResponse, error)
mustEmbedUnimplementedWalletServiceServer()
}
@ -997,6 +1010,9 @@ func (UnimplementedWalletServiceServer) GetRedPacket(context.Context, *GetRedPac
func (UnimplementedWalletServiceServer) ExpireRedPackets(context.Context, *ExpireRedPacketsRequest) (*ExpireRedPacketsResponse, error) {
return nil, status.Error(codes.Unimplemented, "method ExpireRedPackets not implemented")
}
func (UnimplementedWalletServiceServer) RetryRedPacketRefund(context.Context, *RetryRedPacketRefundRequest) (*RetryRedPacketRefundResponse, error) {
return nil, status.Error(codes.Unimplemented, "method RetryRedPacketRefund not implemented")
}
func (UnimplementedWalletServiceServer) mustEmbedUnimplementedWalletServiceServer() {}
func (UnimplementedWalletServiceServer) testEmbeddedByValue() {}
@ -2080,6 +2096,24 @@ func _WalletService_ExpireRedPackets_Handler(srv interface{}, ctx context.Contex
return interceptor(ctx, in, info, handler)
}
func _WalletService_RetryRedPacketRefund_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(RetryRedPacketRefundRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(WalletServiceServer).RetryRedPacketRefund(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: WalletService_RetryRedPacketRefund_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(WalletServiceServer).RetryRedPacketRefund(ctx, req.(*RetryRedPacketRefundRequest))
}
return interceptor(ctx, in, info, handler)
}
// WalletService_ServiceDesc is the grpc.ServiceDesc for WalletService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@ -2323,6 +2357,10 @@ var WalletService_ServiceDesc = grpc.ServiceDesc{
MethodName: "ExpireRedPackets",
Handler: _WalletService_ExpireRedPackets_Handler,
},
{
MethodName: "RetryRedPacketRefund",
Handler: _WalletService_RetryRedPacketRefund_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "proto/wallet/v1/wallet.proto",

View File

@ -0,0 +1,272 @@
# 用户举报 Flutter 对接
本文描述 Flutter App 对接用户举报和房间举报的 HTTP 契约。通用文件上传接口用于上传举报图片并拿到 URL提交举报接口只接收 JSON。
## 基础地址
本地开发地址:
```text
http://127.0.0.1:13000
```
线上环境使用当前 App 配置里的 gateway base URL。下面所有路径都拼在 gateway base URL 后。
## 请求头
| Header | 必填 | 示例 | 说明 |
| --- | --- | --- | --- |
| `Authorization` | 是 | `Bearer <access_token>` | 登录 access token举报发起人只从 token 读取Flutter 不传举报发起人 ID。 |
| `X-App-Code` | 否 | `lalu` | App 编码;登录态接口最终以 token 内的 `app_code` 为准。 |
| `Content-Type` | 提交举报必填 | `application/json` | 提交举报使用 JSON。 |
| `Content-Type` | 上传图片必填 | `multipart/form-data` | 上传举报图片使用 multipart。 |
统一响应 envelope
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_abc",
"data": {}
}
```
Flutter 只在 `code == "OK"` 时读取 `data`。失败时记录 `request_id`,用于后端排查。
## 调用顺序
```text
1. 用户在举报弹窗选择举报类型。
2. 如有举报图,逐张调用 POST /api/v1/files/upload收集每次返回的 data.url。
3. 调用 POST /api/v1/reports 提交举报。
4. 提交成功后关闭弹窗,并用 data.report_id 写本地日志。
```
## 上传举报图片
上传接口只负责把文件写入对象存储并返回 URL不创建举报单。
```http
POST /api/v1/files/upload
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: multipart/form-data
file=<binary>
```
请求字段:
| 字段 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `file` | 是 | multipart file | 单文件最大 20MB。多图逐张上传Flutter 把每张图返回的 `data.url` 放进提交举报接口的 `image_urls`。 |
成功响应:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_upload_report_image",
"data": {
"url": "https://cdn.example.com/app/files/10001/20260529/file_xxx.jpg",
"object_key": "app/files/10001/20260529/file_xxx.jpg",
"content_type": "image/jpeg",
"size_bytes": 345678
}
}
```
Flutter 提交举报时只需要使用 `data.url`
## 提交举报
```http
POST /api/v1/reports
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
{
"user_id": "10002",
"report_type": "pornography",
"reason": "用户在房间内发布违规内容",
"image_urls": [
"https://cdn.example.com/app/files/10001/20260529/file_xxx.jpg"
]
}
```
举报房间示例:
```http
POST /api/v1/reports
Authorization: Bearer <access_token>
X-App-Code: lalu
Content-Type: application/json
{
"room_id": "lalu_abc123",
"report_type": "fraud",
"reason": "房间引导线下交易",
"image_urls": []
}
```
请求字段:
| 字段 | 必填 | 类型 | 说明 |
| --- | --- | --- | --- |
| `user_id` | 条件必填 | string | 被举报用户 ID。和 `room_id` 二选一,只能传一个。 |
| `room_id` | 条件必填 | string | 被举报房间 ID。和 `user_id` 二选一,只能传一个。 |
| `report_type` | 是 | string | 举报类型,取值见下方枚举。 |
| `reason` | 否 | string | 举报理由Flutter 可允许用户不填。 |
| `image_urls` | 否 | string[] | 举报图 URL 数组;无图可传 `[]` 或不传。 |
目标校验规则:
| 场景 | 结果 |
| --- | --- |
| 只传 `user_id` | 举报用户。 |
| 只传 `room_id` | 举报房间。 |
| `user_id``room_id` 都不传 | `400 INVALID_ARGUMENT`。 |
| `user_id``room_id` 同时传 | `400 INVALID_ARGUMENT`Flutter 需要拆成一次明确的用户举报或房间举报。 |
举报发起人不在请求体里传。gateway 必须从 `Authorization` 对应的 access token 读取当前登录用户,避免客户端伪造举报发起人。
## 举报类型
| UI 文案 | `report_type` |
| --- | --- |
| Politics | `politics` |
| Pornography | `pornography` |
| Graphic Violence | `graphic_violence` |
| Verbal Abuse | `verbal_abuse` |
| Fraud | `fraud` |
| Illegal Content | `illegal_content` |
| Minors | `minors` |
| In-Person Transactions | `in_person_transactions` |
| Other | `other` |
## 返回体模拟数据
举报用户成功:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_report_user",
"data": {
"report_id": "rpt_10001_1778000000000",
"target_type": "user",
"user_id": "10002",
"room_id": "",
"report_type": "pornography",
"status": "submitted",
"created_at_ms": 1778000000000
}
}
```
举报房间成功:
```json
{
"code": "OK",
"message": "ok",
"request_id": "req_report_room",
"data": {
"report_id": "rpt_10001_1778000001000",
"target_type": "room",
"user_id": "",
"room_id": "lalu_abc123",
"report_type": "fraud",
"status": "submitted",
"created_at_ms": 1778000001000
}
}
```
字段说明:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `report_id` | string | 举报单 IDFlutter 可用于本地日志和客服排查。 |
| `target_type` | string | `user``room`,由服务端按本次传入目标生成。 |
| `user_id` | string | 被举报用户 ID房间举报返回空字符串。 |
| `room_id` | string | 被举报房间 ID用户举报返回空字符串。 |
| `report_type` | string | 本次提交的举报类型。 |
| `status` | string | 首次提交固定返回 `submitted`。 |
| `created_at_ms` | int64 | 举报创建时间Unix epoch milliseconds。 |
## 错误处理
失败返回仍使用统一 envelope
```json
{
"code": "INVALID_ARGUMENT",
"message": "invalid argument",
"request_id": "req_abc"
}
```
| HTTP 状态码 | `code` | Flutter 处理 |
| --- | --- | --- |
| `400` | `INVALID_JSON` | JSON 格式错误,检查本地序列化逻辑。 |
| `400` | `INVALID_ARGUMENT` | `user_id/room_id` 二选一不满足、`report_type` 非法、图片 URL 格式非法或字段超长。 |
| `401` | `UNAUTHORIZED` | token 缺失、无效、过期或会话失效,重新登录。 |
| `403` | `PROFILE_REQUIRED` | 用户资料未完成,跳转资料补全流程。 |
| `404` | `NOT_FOUND` | 被举报用户不存在,刷新页面状态。 |
| `502` | `UPSTREAM_ERROR` | 内部服务暂不可用,可提示稍后重试。 |
## Flutter 数据模型建议
```dart
class SubmitReportRequest {
SubmitReportRequest({
this.userId,
this.roomId,
required this.reportType,
this.reason,
this.imageUrls = const [],
});
final String? userId;
final String? roomId;
final String reportType;
final String? reason;
final List<String> imageUrls;
Map<String, dynamic> toJson() => {
if (userId != null && userId!.isNotEmpty) 'user_id': userId,
if (roomId != null && roomId!.isNotEmpty) 'room_id': roomId,
'report_type': reportType,
if (reason != null && reason!.trim().isNotEmpty) 'reason': reason!.trim(),
if (imageUrls.isNotEmpty) 'image_urls': imageUrls,
};
}
```
提交前本地校验:
```dart
void validateReportTarget({String? userId, String? roomId}) {
final hasUser = userId != null && userId.trim().isNotEmpty;
final hasRoom = roomId != null && roomId.trim().isNotEmpty;
if (hasUser == hasRoom) {
throw ArgumentError('user_id and room_id must be exactly one');
}
}
```
## 实现边界
- Flutter 不传举报发起人 ID服务端只认 token 中的当前用户。
- `user_id` 表示被举报用户,不是举报发起人。
- `room_id` 表示被举报房间,不要求同时传房间内某个用户。
- `user_id``room_id` 必须二选一,不能同时提交一个混合举报。
- 举报图片先上传再提交 URL提交举报接口不接收 multipart 文件。
- `message` 不作为业务分支依据Flutter 只按 `code` 和 HTTP 状态处理。

View File

@ -47,6 +47,7 @@ import (
redpacketmodule "hyapp-admin-server/internal/modules/redpacket"
regionblockmodule "hyapp-admin-server/internal/modules/regionblock"
registrationrewardmodule "hyapp-admin-server/internal/modules/registrationreward"
reportmodule "hyapp-admin-server/internal/modules/report"
resourcemodule "hyapp-admin-server/internal/modules/resource"
roomadminmodule "hyapp-admin-server/internal/modules/roomadmin"
roomtreasuremodule "hyapp-admin-server/internal/modules/roomtreasure"
@ -227,6 +228,7 @@ func main() {
Payment: paymentmodule.New(walletclient.NewGRPC(walletConn), auditHandler),
RBAC: rbacmodule.New(store, auditHandler),
RedPacket: redpacketmodule.New(walletclient.NewGRPC(walletConn), auditHandler),
Report: reportmodule.New(userDB, roomClient),
RegistrationReward: registrationrewardmodule.New(activityclient.NewGRPC(activityConn), userDB, auditHandler),
RegionBlock: regionblockmodule.New(userDB, auditHandler),
Resource: resourcemodule.New(walletclient.NewGRPC(walletConn), store, userDB, auditHandler),

View File

@ -46,6 +46,7 @@ type Client interface {
UpdateRedPacketConfig(ctx context.Context, req *walletv1.UpdateRedPacketConfigRequest) (*walletv1.UpdateRedPacketConfigResponse, error)
ListRedPackets(ctx context.Context, req *walletv1.ListRedPacketsRequest) (*walletv1.ListRedPacketsResponse, error)
GetRedPacket(ctx context.Context, req *walletv1.GetRedPacketRequest) (*walletv1.GetRedPacketResponse, error)
RetryRedPacketRefund(ctx context.Context, req *walletv1.RetryRedPacketRefundRequest) (*walletv1.RetryRedPacketRefundResponse, error)
}
type GRPCClient struct {
@ -199,3 +200,7 @@ func (c *GRPCClient) ListRedPackets(ctx context.Context, req *walletv1.ListRedPa
func (c *GRPCClient) GetRedPacket(ctx context.Context, req *walletv1.GetRedPacketRequest) (*walletv1.GetRedPacketResponse, error) {
return c.client.GetRedPacket(ctx, req)
}
func (c *GRPCClient) RetryRedPacketRefund(ctx context.Context, req *walletv1.RetryRedPacketRefundRequest) (*walletv1.RetryRedPacketRefundResponse, error) {
return c.client.RetryRedPacketRefund(ctx, req)
}

View File

@ -26,10 +26,16 @@ func New(wallet walletclient.Client, audit shared.OperationLogger) *Handler {
type configRequest struct {
Enabled bool `json:"enabled"`
CountTiers []int32 `json:"count_tiers"`
CountOptions []int32 `json:"countOptions"`
AmountTiers []int64 `json:"amount_tiers"`
AmountOptions []int64 `json:"amountOptions"`
DelayedOpenSeconds int32 `json:"delayed_open_seconds"`
DelaySeconds int32 `json:"delaySeconds"`
ExpireSeconds int32 `json:"expire_seconds"`
DailySendLimit int32 `json:"daily_send_limit"`
DailySendLimitV2 int32 `json:"dailySendLimit"`
RuleURL string `json:"rule_url"`
RuleURLV2 string `json:"ruleUrl"`
}
type configDTO struct {
@ -40,6 +46,7 @@ type configDTO struct {
DelayedOpenSeconds int32 `json:"delayed_open_seconds"`
ExpireSeconds int32 `json:"expire_seconds"`
DailySendLimit int32 `json:"daily_send_limit"`
RuleURL string `json:"rule_url"`
UpdatedByAdminID int64 `json:"updated_by_admin_id"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
@ -62,9 +69,17 @@ type packetDTO struct {
ExpiresAtMS int64 `json:"expires_at_ms"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
RefundAmount int64 `json:"refund_amount"`
RefundStatus string `json:"refund_status"`
RefundedAtMS int64 `json:"refunded_at_ms"`
Claims []claimDTO `json:"claims,omitempty"`
}
type refundRetryRequest struct {
PacketNo string `json:"packetNo"`
PacketID string `json:"packet_id"`
}
type claimDTO struct {
AppCode string `json:"app_code"`
ClaimID string `json:"claim_id"`
@ -104,15 +119,36 @@ func (h *Handler) UpdateConfig(c *gin.Context) {
response.BadRequest(c, "红包配置参数不正确")
return
}
countTiers := req.CountTiers
if len(countTiers) == 0 {
countTiers = req.CountOptions
}
amountTiers := req.AmountTiers
if len(amountTiers) == 0 {
amountTiers = req.AmountOptions
}
delaySeconds := req.DelayedOpenSeconds
if delaySeconds == 0 {
delaySeconds = req.DelaySeconds
}
dailyLimit := req.DailySendLimit
if dailyLimit == 0 {
dailyLimit = req.DailySendLimitV2
}
ruleURL := strings.TrimSpace(req.RuleURL)
if ruleURL == "" {
ruleURL = strings.TrimSpace(req.RuleURLV2)
}
resp, err := h.wallet.UpdateRedPacketConfig(c.Request.Context(), &walletv1.UpdateRedPacketConfigRequest{
AppCode: appctx.FromContext(c.Request.Context()),
Enabled: req.Enabled,
CountTiers: req.CountTiers,
AmountTiers: req.AmountTiers,
DelayedOpenSeconds: req.DelayedOpenSeconds,
ExpireSeconds: req.ExpireSeconds,
DailySendLimit: req.DailySendLimit,
CountTiers: countTiers,
AmountTiers: amountTiers,
DelayedOpenSeconds: delaySeconds,
ExpireSeconds: 86400,
DailySendLimit: dailyLimit,
OperatorUserId: int64(middleware.CurrentUserID(c)),
RuleUrl: ruleURL,
})
if err != nil {
response.BadRequest(c, err.Error())
@ -136,10 +172,16 @@ func (h *Handler) ListPackets(c *gin.Context) {
Status: strings.TrimSpace(options.Status),
Page: int32(options.Page),
PageSize: int32(options.PageSize),
SenderUserId: parseInt64Query(c, "sender_user_id"),
SenderUserId: firstInt64Query(c, "senderUserId", "sender_user_id"),
RegionId: parseInt64Query(c, "region_id"),
StartAtMs: parseInt64Query(c, "start_at_ms"),
EndAtMs: parseInt64Query(c, "end_at_ms"),
StartAtMs: firstInt64Query(c, "startTime", "start_at_ms"),
EndAtMs: firstInt64Query(c, "endTime", "end_at_ms"),
}
if roomID := strings.TrimSpace(c.Query("roomId")); roomID != "" {
req.RoomId = roomID
}
if packetMode := strings.TrimSpace(c.Query("packetMode")); packetMode != "" {
req.PacketType = packetMode
}
resp, err := h.wallet.ListRedPackets(c.Request.Context(), req)
if err != nil {
@ -175,6 +217,41 @@ func (h *Handler) GetPacket(c *gin.Context) {
response.OK(c, packetFromProto(resp.GetPacket()))
}
func (h *Handler) RetryRefund(c *gin.Context) {
if h.wallet == nil {
response.ServerError(c, "钱包服务未配置")
return
}
var req refundRetryRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "红包退款参数不正确")
return
}
packetID := strings.TrimSpace(req.PacketNo)
if packetID == "" {
packetID = strings.TrimSpace(req.PacketID)
}
if packetID == "" {
response.BadRequest(c, "红包 ID 不正确")
return
}
resp, err := h.wallet.RetryRedPacketRefund(c.Request.Context(), &walletv1.RetryRedPacketRefundRequest{
AppCode: appctx.FromContext(c.Request.Context()),
PacketId: packetID,
OperatorUserId: int64(middleware.CurrentUserID(c)),
})
if err != nil {
response.BadRequest(c, err.Error())
return
}
shared.OperationLogWithResourceID(c, h.audit, "retry-red-packet-refund", "red_packets", packetID, "success", "")
response.OK(c, gin.H{
"packet": packetFromProto(resp.GetPacket()),
"refundedAmount": resp.GetRefundedAmount(),
"serverTime": resp.GetServerTimeMs(),
})
}
func configFromProto(config *walletv1.RedPacketConfig) configDTO {
if config == nil {
return configDTO{}
@ -187,6 +264,7 @@ func configFromProto(config *walletv1.RedPacketConfig) configDTO {
DelayedOpenSeconds: config.GetDelayedOpenSeconds(),
ExpireSeconds: config.GetExpireSeconds(),
DailySendLimit: config.GetDailySendLimit(),
RuleURL: config.GetRuleUrl(),
UpdatedByAdminID: config.GetUpdatedByAdminId(),
CreatedAtMS: config.GetCreatedAtMs(),
UpdatedAtMS: config.GetUpdatedAtMs(),
@ -218,6 +296,9 @@ func packetFromProto(packet *walletv1.RedPacket) packetDTO {
ExpiresAtMS: packet.GetExpiresAtMs(),
CreatedAtMS: packet.GetCreatedAtMs(),
UpdatedAtMS: packet.GetUpdatedAtMs(),
RefundAmount: packet.GetRefundAmount(),
RefundStatus: packet.GetRefundStatus(),
RefundedAtMS: packet.GetRefundedAtMs(),
Claims: claims,
}
}
@ -241,6 +322,15 @@ func claimFromProto(claim *walletv1.RedPacketClaim) claimDTO {
}
}
func firstInt64Query(c *gin.Context, keys ...string) int64 {
for _, key := range keys {
if value := parseInt64Query(c, key); value > 0 {
return value
}
}
return 0
}
func parseInt64Query(c *gin.Context, key string) int64 {
value := strings.TrimSpace(c.Query(key))
if value == "" {

View File

@ -10,8 +10,9 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
if h == nil {
return
}
protected.GET("/admin/activity/red-packets/config", middleware.RequirePermission("red-packet:view"), h.GetConfig)
protected.PUT("/admin/activity/red-packets/config", middleware.RequirePermission("red-packet:update"), h.UpdateConfig)
protected.GET("/admin/activity/red-packets", middleware.RequirePermission("red-packet:view"), h.ListPackets)
protected.GET("/admin/activity/red-packets/:packet_id", middleware.RequirePermission("red-packet:view"), h.GetPacket)
protected.GET("/resident-activity/voice-room-red-packet/config", middleware.RequirePermission("red-packet:view"), h.GetConfig)
protected.POST("/resident-activity/voice-room-red-packet/config", middleware.RequirePermission("red-packet:update"), h.UpdateConfig)
protected.GET("/resident-activity/voice-room-red-packet/records", middleware.RequirePermission("red-packet:view"), h.ListPackets)
protected.GET("/resident-activity/voice-room-red-packet/records/:packet_id", middleware.RequirePermission("red-packet:view"), h.GetPacket)
protected.POST("/resident-activity/voice-room-red-packet/refund/retry", middleware.RequirePermission("red-packet:update"), h.RetryRefund)
}

View File

@ -0,0 +1,83 @@
package report
import (
"database/sql"
"strconv"
"strings"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/integration/roomclient"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/response"
"github.com/gin-gonic/gin"
)
type Handler struct {
service *Service
}
func New(userDB *sql.DB, roomClient roomclient.Client) *Handler {
return &Handler{service: NewService(userDB, roomClient)}
}
func (h *Handler) ListReports(c *gin.Context) {
query, ok := parseListQuery(c)
if !ok {
return
}
items, total, err := h.service.ListReports(c.Request.Context(), appctx.FromContext(c.Request.Context()), query)
if err != nil {
response.ServerError(c, "获取举报列表失败")
return
}
response.OK(c, response.Page{Items: items, Page: query.Page, PageSize: query.PageSize, Total: total})
}
func parseListQuery(c *gin.Context) (listQuery, bool) {
options := shared.ListOptions(c)
startAtMS, ok := optionalInt64(c, "start_at_ms", "startAtMs")
if !ok {
response.BadRequest(c, "开始时间不正确")
return listQuery{}, false
}
endAtMS, ok := optionalInt64(c, "end_at_ms", "endAtMs")
if !ok {
response.BadRequest(c, "结束时间不正确")
return listQuery{}, false
}
if startAtMS > 0 && endAtMS > 0 && startAtMS >= endAtMS {
response.BadRequest(c, "时间区间不正确")
return listQuery{}, false
}
return normalizeListQuery(listQuery{
Page: options.Page,
PageSize: options.PageSize,
Keyword: options.Keyword,
TargetType: firstQuery(c, "target_type", "targetType"),
ReportType: firstQuery(c, "report_type", "reportType"),
StartAtMS: startAtMS,
EndAtMS: endAtMS,
}), true
}
func optionalInt64(c *gin.Context, keys ...string) (int64, bool) {
value := strings.TrimSpace(firstQuery(c, keys...))
if value == "" {
return 0, true
}
parsed, err := strconv.ParseInt(value, 10, 64)
if err != nil || parsed < 0 {
return 0, false
}
return parsed, true
}
func firstQuery(c *gin.Context, keys ...string) string {
for _, key := range keys {
if value := strings.TrimSpace(c.Query(key)); value != "" {
return value
}
}
return ""
}

View File

@ -0,0 +1,11 @@
package report
type listQuery struct {
Page int
PageSize int
Keyword string
TargetType string
ReportType string
StartAtMS int64
EndAtMS int64
}

View File

@ -0,0 +1,38 @@
package report
type reportUserDTO struct {
Avatar string `json:"avatar"`
DefaultDisplayUserID string `json:"defaultDisplayUserId"`
DisplayUserID string `json:"displayUserId"`
UserID string `json:"userId"`
Username string `json:"username"`
}
type reportRoomDTO struct {
CoverURL string `json:"coverUrl"`
RoomID string `json:"roomId"`
RoomShortID string `json:"roomShortId"`
Title string `json:"title"`
}
type reportTargetDTO struct {
Type string `json:"type"`
Room *reportRoomDTO `json:"room,omitempty"`
User *reportUserDTO `json:"user,omitempty"`
}
type reportDTO struct {
CreatedAtMS int64 `json:"createdAtMs"`
ImageURLs []string `json:"imageUrls"`
Reason string `json:"reason"`
ReportID string `json:"reportId"`
ReporterUserID string `json:"reporterUserId"`
ReportType string `json:"reportType"`
RequestID string `json:"requestId"`
RoomID string `json:"roomId"`
Status string `json:"status"`
Target reportTargetDTO `json:"target"`
TargetType string `json:"targetType"`
UpdatedAtMS int64 `json:"updatedAtMs"`
UserID string `json:"userId"`
}

View File

@ -0,0 +1,15 @@
package report
import (
"hyapp-admin-server/internal/middleware"
"github.com/gin-gonic/gin"
)
func RegisterRoutes(protected *gin.RouterGroup, h *Handler) {
if h == nil {
return
}
protected.GET("/admin/operations/reports", middleware.RequirePermission("report:view"), h.ListReports)
}

View File

@ -0,0 +1,274 @@
package report
import (
"context"
"database/sql"
"encoding/json"
"fmt"
"strconv"
"strings"
"hyapp-admin-server/internal/integration/roomclient"
)
const (
targetTypeUser = "user"
targetTypeRoom = "room"
)
type Service struct {
roomClient roomclient.Client
userDB *sql.DB
}
func NewService(userDB *sql.DB, roomClient roomclient.Client) *Service {
return &Service{userDB: userDB, roomClient: roomClient}
}
func (s *Service) ListReports(ctx context.Context, appCode string, query listQuery) ([]reportDTO, int64, error) {
query = normalizeListQuery(query)
if s == nil || s.userDB == nil {
return nil, 0, fmt.Errorf("user mysql is not configured")
}
whereSQL, args := reportWhere(appCode, query)
var total int64
if err := s.userDB.QueryRowContext(ctx, `
SELECT COUNT(*)
FROM user_reports r
LEFT JOIN users u ON u.app_code = r.app_code AND u.user_id = r.target_user_id
`+whereSQL,
args...,
).Scan(&total); err != nil {
return nil, 0, err
}
rows, err := s.userDB.QueryContext(ctx, `
SELECT r.report_id, r.reporter_user_id, r.target_type, r.target_user_id, r.room_id,
r.report_type, r.reason, CAST(r.image_urls_json AS CHAR), r.status, r.request_id,
r.created_at_ms, r.updated_at_ms,
COALESCE(u.current_display_user_id, ''), COALESCE(u.default_display_user_id, ''),
COALESCE(u.username, ''), COALESCE(u.avatar, '')
FROM user_reports r
LEFT JOIN users u ON u.app_code = r.app_code AND u.user_id = r.target_user_id
`+whereSQL+`
ORDER BY r.created_at_ms DESC, r.report_id DESC
LIMIT ? OFFSET ?`,
append(args, query.PageSize, offset(query.Page, query.PageSize))...,
)
if err != nil {
return nil, 0, err
}
defer rows.Close()
items := make([]reportDTO, 0, query.PageSize)
for rows.Next() {
var item reportDTO
var reporterUserID int64
var targetUserID sql.NullInt64
var imageURLsJSON string
var displayUserID string
var defaultDisplayUserID string
var username string
var avatar string
if err := rows.Scan(
&item.ReportID,
&reporterUserID,
&item.TargetType,
&targetUserID,
&item.RoomID,
&item.ReportType,
&item.Reason,
&imageURLsJSON,
&item.Status,
&item.RequestID,
&item.CreatedAtMS,
&item.UpdatedAtMS,
&displayUserID,
&defaultDisplayUserID,
&username,
&avatar,
); err != nil {
return nil, 0, err
}
imageURLs, err := parseImageURLs(imageURLsJSON)
if err != nil {
return nil, 0, err
}
item.ReporterUserID = formatID(reporterUserID)
item.ImageURLs = imageURLs
item.Target = reportTargetDTO{Type: item.TargetType}
if targetUserID.Valid && targetUserID.Int64 > 0 {
item.UserID = formatID(targetUserID.Int64)
item.Target.User = &reportUserDTO{
Avatar: avatar,
DefaultDisplayUserID: defaultDisplayUserID,
DisplayUserID: displayUserID,
UserID: item.UserID,
Username: username,
}
}
if item.TargetType == targetTypeRoom {
item.Target.Room = &reportRoomDTO{RoomID: item.RoomID}
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, 0, err
}
if err := s.fillRoomTargets(ctx, items); err != nil {
return nil, 0, err
}
return items, total, nil
}
func normalizeListQuery(query listQuery) listQuery {
if query.Page < 1 {
query.Page = 1
}
if query.PageSize < 1 {
query.PageSize = 20
}
if query.PageSize > 100 {
query.PageSize = 100
}
query.Keyword = strings.TrimSpace(query.Keyword)
query.TargetType = normalizeTargetType(query.TargetType)
query.ReportType = strings.ToLower(strings.TrimSpace(query.ReportType))
if query.StartAtMS < 0 {
query.StartAtMS = 0
}
if query.EndAtMS < 0 {
query.EndAtMS = 0
}
return query
}
func normalizeTargetType(value string) string {
switch strings.ToLower(strings.TrimSpace(value)) {
case targetTypeUser, "用户":
return targetTypeUser
case targetTypeRoom, "房间":
return targetTypeRoom
default:
return ""
}
}
func reportWhere(appCode string, query listQuery) (string, []any) {
conditions := []string{"r.app_code = ?"}
args := []any{appCode}
if query.TargetType != "" {
conditions = append(conditions, "r.target_type = ?")
args = append(args, query.TargetType)
}
if query.ReportType != "" {
conditions = append(conditions, "r.report_type = ?")
args = append(args, query.ReportType)
}
if query.StartAtMS > 0 {
conditions = append(conditions, "r.created_at_ms >= ?")
args = append(args, query.StartAtMS)
}
if query.EndAtMS > 0 {
conditions = append(conditions, "r.created_at_ms < ?")
args = append(args, query.EndAtMS)
}
if query.Keyword != "" {
like := "%" + query.Keyword + "%"
conditions = append(conditions, `(r.report_id LIKE ? OR r.room_id LIKE ? OR CAST(r.target_user_id AS CHAR) LIKE ? OR CAST(r.reporter_user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.default_display_user_id LIKE ? OR u.username LIKE ?)`)
args = append(args, like, like, like, like, like, like, like)
}
return "WHERE " + strings.Join(conditions, " AND "), args
}
func parseImageURLs(raw string) ([]string, error) {
if strings.TrimSpace(raw) == "" {
return []string{}, nil
}
var urls []string
if err := json.Unmarshal([]byte(raw), &urls); err != nil {
return nil, err
}
if urls == nil {
return []string{}, nil
}
return urls, nil
}
func (s *Service) fillRoomTargets(ctx context.Context, items []reportDTO) error {
if s == nil || s.roomClient == nil || len(items) == 0 {
return nil
}
roomIDs := make([]string, 0, len(items))
for _, item := range items {
if item.TargetType == targetTypeRoom && strings.TrimSpace(item.RoomID) != "" {
roomIDs = append(roomIDs, item.RoomID)
}
}
rooms, err := s.queryRooms(ctx, uniqueStrings(roomIDs))
if err != nil {
return err
}
for index := range items {
if items[index].TargetType != targetTypeRoom {
continue
}
room := reportRoomDTO{RoomID: items[index].RoomID}
if found, ok := rooms[items[index].RoomID]; ok {
room = found
}
items[index].Target.Room = &room
}
return nil
}
func (s *Service) queryRooms(ctx context.Context, roomIDs []string) (map[string]reportRoomDTO, error) {
rooms := make(map[string]reportRoomDTO, len(roomIDs))
for _, roomID := range roomIDs {
room, err := s.roomClient.GetRoom(ctx, roomclient.GetRoomRequest{RoomID: roomID})
if err != nil {
// 举报列表是治理入口,房间被删或房间服务短暂不可读时仍保留举报事实本身。
if ctx.Err() != nil {
return nil, ctx.Err()
}
continue
}
rooms[roomID] = reportRoomDTO{
CoverURL: room.CoverURL,
RoomID: room.RoomID,
RoomShortID: room.RoomShortID,
Title: room.Title,
}
}
return rooms, nil
}
func formatID(value int64) string {
if value <= 0 {
return ""
}
return strconv.FormatInt(value, 10)
}
func uniqueStrings(values []string) []string {
seen := make(map[string]struct{}, len(values))
unique := make([]string, 0, len(values))
for _, value := range values {
value = strings.TrimSpace(value)
if value == "" {
continue
}
if _, ok := seen[value]; ok {
continue
}
seen[value] = struct{}{}
unique = append(unique, value)
}
return unique
}
func offset(page int, pageSize int) int {
return (page - 1) * pageSize
}

View File

@ -0,0 +1,38 @@
package report
import "testing"
func TestNormalizeListQueryCapsPageSizeAndFilters(t *testing.T) {
query := normalizeListQuery(listQuery{Page: -1, PageSize: 1000, Keyword: " abc ", TargetType: "用户", ReportType: " Fraud "})
if query.Page != 1 || query.PageSize != 100 || query.Keyword != "abc" || query.TargetType != targetTypeUser || query.ReportType != "fraud" {
t.Fatalf("normalized query mismatch: %+v", query)
}
}
func TestReportWhereUsesStableFilters(t *testing.T) {
query := listQuery{
Keyword: "10001",
TargetType: targetTypeRoom,
ReportType: "fraud",
StartAtMS: 100,
EndAtMS: 200,
}
where, args := reportWhere("lalu", query)
want := "WHERE r.app_code = ? AND r.target_type = ? AND r.report_type = ? AND r.created_at_ms >= ? AND r.created_at_ms < ? AND (r.report_id LIKE ? OR r.room_id LIKE ? OR CAST(r.target_user_id AS CHAR) LIKE ? OR CAST(r.reporter_user_id AS CHAR) LIKE ? OR u.current_display_user_id LIKE ? OR u.default_display_user_id LIKE ? OR u.username LIKE ?)"
if where != want {
t.Fatalf("where mismatch:\nwant %s\n got %s", want, where)
}
if len(args) != 12 || args[0] != "lalu" || args[1] != targetTypeRoom || args[2] != "fraud" || args[3] != int64(100) || args[4] != int64(200) || args[5] != "%10001%" || args[11] != "%10001%" {
t.Fatalf("args mismatch: %#v", args)
}
}
func TestParseImageURLsNormalizesEmptyJSON(t *testing.T) {
urls, err := parseImageURLs("null")
if err != nil {
t.Fatalf("parse images failed: %v", err)
}
if len(urls) != 0 {
t.Fatalf("empty images mismatch: %#v", urls)
}
}

View File

@ -76,6 +76,7 @@ var defaultPermissions = []model.Permission{
{Name: "金币流水查看", Code: "coin-ledger:view", Kind: "menu"},
{Name: "金币增减查看", Code: "coin-adjustment:view", Kind: "menu"},
{Name: "金币增减创建", Code: "coin-adjustment:create", Kind: "button"},
{Name: "举报列表查看", Code: "report:view", Kind: "menu"},
{Name: "支付账单查看", Code: "payment-bill:view", Kind: "menu"},
{Name: "内购配置查看", Code: "payment-product:view", Kind: "menu"},
{Name: "内购配置创建", Code: "payment-product:create", Kind: "button"},
@ -238,6 +239,7 @@ func (s *Store) seedMenus() error {
{ParentID: &operationsID, Title: "金币流水", Code: "operation-coin-ledger", Path: "/operations/coin-ledger", Icon: "receipt", PermissionCode: "coin-ledger:view", Sort: 68, Visible: true},
{ParentID: &operationsID, Title: "金币增减", Code: "operation-coin-adjustment", Path: "/operations/coin-adjustments", Icon: "wallet", PermissionCode: "coin-adjustment:view", Sort: 69, Visible: true},
{ParentID: &operationsID, Title: "幸运礼物", Code: "lucky-gift", Path: "/operations/lucky-gift", Icon: "redeem", PermissionCode: "lucky-gift:view", Sort: 70, Visible: true},
{ParentID: &operationsID, Title: "举报列表", Code: "operation-reports", Path: "/operations/reports", Icon: "flag", PermissionCode: "report:view", Sort: 71, Visible: true},
{ParentID: &paymentID, Title: "账单列表", Code: "payment-bill-list", Path: "/payment/bills", Icon: "receipt", PermissionCode: "payment-bill:view", Sort: 68, Visible: true},
{ParentID: &activityID, Title: "每日任务", Code: "daily-task-list", Path: "/activities/daily-tasks", Icon: "task", PermissionCode: "daily-task:view", Sort: 69, Visible: true},
{ParentID: &activityID, Title: "注册奖励", Code: "registration-reward", Path: "/activities/registration-reward", Icon: "gift", PermissionCode: "registration-reward:view", Sort: 70, Visible: true},
@ -482,7 +484,7 @@ func defaultRolePermissionCodes(code string) []string {
"agency:view", "agency:create", "agency:status",
"bd:view", "bd:create", "bd:update",
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit",
"coin-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "payment-bill:view", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
"coin-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "payment-bill:view", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
"lucky-gift:view", "lucky-gift:update",
"game:view", "game:create", "game:update", "game:status", "game:delete",
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
@ -496,7 +498,7 @@ func defaultRolePermissionCodes(code string) []string {
"upload:create",
}
case "auditor":
return []string{"overview:view", "log:view", "user:view", "app-user:view", "level-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "coin-ledger:view", "coin-adjustment:view", "lucky-gift:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-treasure:view", "red-packet:view", "vip-config:view", "role:view", "permission:view", "job:view"}
return []string{"overview:view", "log:view", "user:view", "app-user:view", "level-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "coin-ledger:view", "coin-adjustment:view", "report:view", "lucky-gift:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-treasure:view", "red-packet:view", "vip-config:view", "role:view", "permission:view", "job:view"}
case "readonly":
return []string{
"overview:view",
@ -523,6 +525,7 @@ func defaultRolePermissionCodes(code string) []string {
"coin-seller:view",
"coin-ledger:view",
"coin-adjustment:view",
"report:view",
"lucky-gift:view",
"payment-bill:view",
"payment-product:view",
@ -564,7 +567,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
"country:view", "country:create", "country:update", "country:status",
"region:view", "region:create", "region:update", "region:status",
"coin-seller:view", "coin-seller:create", "coin-seller:update", "coin-seller:stock-credit",
"coin-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "payment-bill:view", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
"coin-ledger:view", "coin-adjustment:view", "coin-adjustment:create", "report:view", "payment-bill:view", "payment-product:view", "payment-product:create", "payment-product:update", "payment-product:delete",
"lucky-gift:view", "lucky-gift:update",
"game:view", "game:create", "game:update", "game:status", "game:delete",
"daily-task:view", "daily-task:create", "daily-task:update", "daily-task:status",
@ -575,7 +578,7 @@ func defaultRolePermissionMigrationCodes(code string) []string {
"vip-config:view", "vip-config:update", "vip-config:grant",
}
case "auditor", "readonly":
return []string{"level-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "coin-seller:view", "coin-ledger:view", "coin-adjustment:view", "lucky-gift:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-treasure:view", "red-packet:view", "vip-config:view"}
return []string{"level-config:view", "region-block:view", "room:view", "room-pin:view", "room-config:view", "app-config:view", "app-version:view", "resource:view", "resource-shop:view", "resource-group:view", "resource-grant:view", "gift:view", "emoji-pack:view", "coin-seller:view", "coin-ledger:view", "coin-adjustment:view", "report:view", "lucky-gift:view", "payment-bill:view", "payment-product:view", "game:view", "daily-task:view", "achievement:view", "seven-day-checkin:view", "room-treasure:view", "red-packet:view", "vip-config:view"}
default:
return nil
}

View File

@ -27,6 +27,7 @@ import (
"hyapp-admin-server/internal/modules/redpacket"
"hyapp-admin-server/internal/modules/regionblock"
"hyapp-admin-server/internal/modules/registrationreward"
reportmodule "hyapp-admin-server/internal/modules/report"
resourcemodule "hyapp-admin-server/internal/modules/resource"
"hyapp-admin-server/internal/modules/roomadmin"
"hyapp-admin-server/internal/modules/roomtreasure"
@ -64,6 +65,7 @@ type Handlers struct {
Payment *payment.Handler
RBAC *rbac.Handler
RedPacket *redpacket.Handler
Report *reportmodule.Handler
RegistrationReward *registrationreward.Handler
RegionBlock *regionblock.Handler
Resource *resourcemodule.Handler
@ -93,6 +95,7 @@ func New(cfg config.Config, auth *service.AuthService, h Handlers) *gin.Engine {
menu.RegisterRoutes(protected, h.Menu)
rbac.RegisterRoutes(protected, h.RBAC)
redpacket.RegisterRoutes(protected, h.RedPacket)
reportmodule.RegisterRoutes(protected, h.Report)
resourcemodule.RegisterRoutes(protected, h.Resource)
adminuser.RegisterRoutes(protected, h.AdminUser)
appuser.RegisterRoutes(protected, h.AppUser)

View File

@ -0,0 +1,43 @@
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
('举报列表查看', 'report:view', 'menu', '', @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
kind = VALUES(kind),
description = VALUES(description),
updated_at_ms = @now_ms;
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms) VALUES
(NULL, '运营管理', 'operations', '', 'operations', '', 68, TRUE, @now_ms, @now_ms)
ON DUPLICATE KEY UPDATE
title = VALUES(title),
path = VALUES(path),
icon = VALUES(icon),
permission_code = VALUES(permission_code),
sort = VALUES(sort),
visible = VALUES(visible),
updated_at_ms = @now_ms;
INSERT INTO admin_menus (parent_id, title, code, path, icon, permission_code, sort, visible, created_at_ms, updated_at_ms)
SELECT parent.id, '举报列表', 'operation-reports', '/operations/reports', 'flag', 'report:view', 71, TRUE, @now_ms, @now_ms
FROM admin_menus parent
WHERE parent.code = 'operations'
ON DUPLICATE KEY UPDATE
parent_id = VALUES(parent_id),
title = VALUES(title),
path = VALUES(path),
icon = VALUES(icon),
permission_code = VALUES(permission_code),
sort = VALUES(sort),
visible = VALUES(visible),
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', 'auditor', 'readonly')
AND admin_permission.code = 'report:view';

View File

@ -235,7 +235,7 @@ func New(cfg config.Config) (*App, error) {
if err != nil || !ok {
return err
}
return broadcastSvc.ConsumeRedPacketCreatedEvent(appcode.WithContext(ctx, event.AppCode), event)
return broadcastSvc.ConsumeRedPacketWalletEvent(appcode.WithContext(ctx, event.AppCode), event)
}); err != nil {
_ = consumer.Shutdown()
shutdownConsumers(mqConsumers)
@ -436,25 +436,32 @@ func firstRechargeEventFromWalletMessage(body []byte) (firstrechargedomain.Recha
}, true, nil
}
func redPacketEventFromWalletMessage(body []byte) (broadcastdomain.RedPacketCreatedEvent, bool, error) {
func redPacketEventFromWalletMessage(body []byte) (broadcastdomain.RedPacketWalletEvent, bool, error) {
message, err := walletmq.DecodeWalletOutboxMessage(body)
if err != nil {
return broadcastdomain.RedPacketCreatedEvent{}, false, err
return broadcastdomain.RedPacketWalletEvent{}, false, err
}
if message.EventType != "WalletRedPacketCreated" {
return broadcastdomain.RedPacketCreatedEvent{}, false, nil
if message.EventType != "WalletRedPacketCreated" && message.EventType != "WalletRedPacketClaimed" && message.EventType != "WalletRedPacketRefunded" {
return broadcastdomain.RedPacketWalletEvent{}, false, nil
}
regionID := redPacketRegionIDFromWalletPayload(message.PayloadJSON)
if regionID <= 0 {
return broadcastdomain.RedPacketCreatedEvent{}, false, nil
packetID, packetType, roomID, regionID, status, openAtMS, expiresAtMS := redPacketFieldsFromWalletPayload(message.PayloadJSON, message.EventType)
if packetID == "" {
return broadcastdomain.RedPacketWalletEvent{}, false, nil
}
return broadcastdomain.RedPacketCreatedEvent{
return broadcastdomain.RedPacketWalletEvent{
AppCode: appcode.Normalize(message.AppCode),
EventID: message.EventID,
EventType: message.EventType,
TransactionID: message.TransactionID,
CommandID: message.CommandID,
PayloadJSON: message.PayloadJSON,
PacketID: packetID,
PacketType: packetType,
RoomID: roomID,
RegionID: regionID,
Status: status,
OpenAtMS: openAtMS,
ExpiresAtMS: expiresAtMS,
CreatedAtMS: message.OccurredAtMS,
}, true, nil
}
@ -480,19 +487,41 @@ func rechargeFactFromWalletPayload(payload string) (int64, string) {
return sequence, rechargeType
}
func redPacketRegionIDFromWalletPayload(payload string) int64 {
func redPacketFieldsFromWalletPayload(payload string, eventType string) (string, string, string, int64, string, int64, int64) {
var decoded map[string]any
if err := json.Unmarshal([]byte(payload), &decoded); err != nil {
return 0
return "", "", "", 0, "", 0, 0
}
switch value := decoded["region_id"].(type) {
packetID := stringFromDecoded(decoded, "packet_id")
if packetID == "" {
packetID = stringFromDecoded(decoded, "packetId")
}
packetType := stringFromDecoded(decoded, "packet_type")
roomID := stringFromDecoded(decoded, "room_id")
regionID := int64FromDecoded(decoded, "region_id")
status := stringFromDecoded(decoded, "status")
openAtMS := int64FromDecoded(decoded, "open_at_ms")
expiresAtMS := int64FromDecoded(decoded, "expires_at_ms")
if eventType == "WalletRedPacketClaimed" || eventType == "WalletRedPacketRefunded" {
status = stringFromDecoded(decoded, "packet_status")
}
return packetID, packetType, roomID, regionID, status, openAtMS, expiresAtMS
}
func stringFromDecoded(decoded map[string]any, key string) string {
value, _ := decoded[key].(string)
return value
}
func int64FromDecoded(decoded map[string]any, key string) int64 {
switch value := decoded[key].(type) {
case float64:
return int64(value)
case int64:
return value
case json.Number:
regionID, _ := value.Int64()
return regionID
parsed, _ := value.Int64()
return parsed
default:
return 0
}

View File

@ -7,6 +7,8 @@ const (
ScopeGlobal = "global"
// ScopeRegion 表示用户所在区域群;贵重礼物、区域红包、区域运营通知默认走这个范围。
ScopeRegion = "region"
// ScopeRoom 表示当前语音房 IM 群。
ScopeRoom = "room"
// TypeSuperGift 是贵重礼物播报,来源于 room-service 已提交的 RoomGiftSent 事实。
TypeSuperGift = "super_gift"
@ -74,13 +76,23 @@ type ConsumeRoomEventResult struct {
BroadcastCreated bool
}
// RedPacketCreatedEvent 是 wallet-service 已提交的红包创建事实。
type RedPacketCreatedEvent struct {
// RedPacketWalletEvent 是 wallet-service 已提交的红包资金事实。
type RedPacketWalletEvent struct {
AppCode string
EventID string
EventType string
TransactionID string
CommandID string
PayloadJSON string
PacketID string
PacketType string
RoomID string
RegionID int64
Status string
OpenAtMS int64
ExpiresAtMS int64
CreatedAtMS int64
}
// RedPacketCreatedEvent keeps older tests and callers compiling while wallet red-packet facts are generalized.
type RedPacketCreatedEvent = RedPacketWalletEvent

View File

@ -2,24 +2,110 @@ package broadcast
import (
"context"
"encoding/json"
"strings"
"hyapp/pkg/appcode"
broadcastdomain "hyapp/services/activity-service/internal/domain/broadcast"
)
// ConsumeRedPacketCreatedEvent converts one MQ-delivered wallet red-packet fact into a broadcast outbox.
func (s *Service) ConsumeRedPacketCreatedEvent(ctx context.Context, event broadcastdomain.RedPacketCreatedEvent) error {
// ConsumeRedPacketWalletEvent converts one MQ-delivered wallet red-packet fact into IM outbox records.
func (s *Service) ConsumeRedPacketWalletEvent(ctx context.Context, event broadcastdomain.RedPacketWalletEvent) error {
if s == nil {
return nil
}
if event.RegionID <= 0 {
ctx = appcode.WithContext(ctx, event.AppCode)
switch event.EventType {
case "WalletRedPacketCreated":
return s.consumeRedPacketCreated(ctx, event)
case "WalletRedPacketClaimed":
if strings.EqualFold(event.Status, "finished") {
return s.publishRedPacketRoomEvent(ctx, event, "STATUS_CHANGED", event.CreatedAtMS)
}
case "WalletRedPacketRefunded":
return s.publishRedPacketRoomEvent(ctx, event, "STATUS_CHANGED", event.CreatedAtMS)
}
return nil
}
func (s *Service) ConsumeRedPacketCreatedEvent(ctx context.Context, event broadcastdomain.RedPacketWalletEvent) error {
if event.EventType == "" {
event.EventType = "WalletRedPacketCreated"
}
return s.ConsumeRedPacketWalletEvent(ctx, event)
}
func (s *Service) consumeRedPacketCreated(ctx context.Context, event broadcastdomain.RedPacketWalletEvent) error {
if event.RoomID == "" {
return nil
}
_, err := s.PublishRegionBroadcast(appcode.WithContext(ctx, event.AppCode), PublishInput{
if err := s.publishRedPacketRoomEvent(ctx, event, "CREATED", event.CreatedAtMS); err != nil {
return err
}
if strings.EqualFold(event.PacketType, "delayed") && event.RegionID > 0 {
if _, err := s.PublishRegionBroadcast(ctx, PublishInput{
EventID: event.EventID + ":region_created",
BroadcastType: broadcastdomain.TypeRedPacket,
RegionID: event.RegionID,
PayloadJSON: redPacketIMPayload(event, "CREATED", "REGION"),
}); err != nil {
return err
}
}
if strings.EqualFold(event.PacketType, "delayed") && event.OpenAtMS > 0 {
_, err := s.PublishRoomBroadcast(ctx, event.RoomID, PublishInput{
EventID: event.EventID + ":open_due",
BroadcastType: broadcastdomain.TypeRedPacket,
RegionID: event.RegionID,
PayloadJSON: redPacketIMPayload(event, "OPEN_DUE", "ROOM"),
DeliverAtMS: event.OpenAtMS,
})
return err
}
return nil
}
func (s *Service) publishRedPacketRoomEvent(ctx context.Context, event broadcastdomain.RedPacketWalletEvent, eventName string, fallbackDeliverAtMS int64) error {
if event.RoomID == "" {
return nil
}
_, err := s.PublishRoomBroadcast(ctx, event.RoomID, PublishInput{
EventID: event.EventID,
BroadcastType: broadcastdomain.TypeRedPacket,
RegionID: event.RegionID,
PayloadJSON: event.PayloadJSON,
PayloadJSON: redPacketIMPayload(event, eventName, "ROOM"),
DeliverAtMS: fallbackDeliverAtMS,
})
return err
}
func redPacketIMPayload(event broadcastdomain.RedPacketWalletEvent, eventName string, scope string) string {
packetMode := "IMMEDIATE"
if strings.EqualFold(event.PacketType, "delayed") {
packetMode = "DELAYED"
}
status := strings.ToUpper(strings.TrimSpace(event.Status))
if status == "" {
status = "ACTIVE"
}
payload := map[string]any{
"type": "ROOM_RED_PACKET",
"data": map[string]any{
"event": eventName,
"scope": scope,
"packetNo": event.PacketID,
"roomId": event.RoomID,
"regionId": event.RegionID,
"packetMode": packetMode,
"status": status,
"claimStartTime": event.OpenAtMS,
"expireTime": event.ExpiresAtMS,
"serverTime": event.CreatedAtMS,
},
}
encoded, err := json.Marshal(payload)
if err != nil {
return `{"type":"ROOM_RED_PACKET","data":{}}`
}
return string(encoded)
}

View File

@ -65,6 +65,7 @@ type PublishInput struct {
BroadcastType string
RegionID int64
PayloadJSON string
DeliverAtMS int64
}
// Service 拥有播报群建群补偿、播报入队和 outbox 投递决策。
@ -146,6 +147,14 @@ func (s *Service) PublishRegionBroadcast(ctx context.Context, input PublishInput
return s.enqueue(ctx, broadcastdomain.ScopeRegion, groupID, input)
}
func (s *Service) PublishRoomBroadcast(ctx context.Context, roomID string, input PublishInput) (broadcastdomain.PublishResult, error) {
roomID = strings.TrimSpace(roomID)
if roomID == "" {
return broadcastdomain.PublishResult{}, xerr.New(xerr.InvalidArgument, "room_id is required")
}
return s.enqueue(ctx, broadcastdomain.ScopeRoom, roomID, input)
}
// PublishGlobalBroadcast 把全局播报写入持久化 outbox。
// 全局播报和区域播报共用同一张表scope/group_id 决定最终投递目标。
func (s *Service) PublishGlobalBroadcast(ctx context.Context, input PublishInput) (broadcastdomain.PublishResult, error) {
@ -209,9 +218,13 @@ func (s *Service) enqueue(ctx context.Context, scope string, groupID string, inp
BroadcastType: input.BroadcastType,
PayloadJSON: payloadJSON,
Status: broadcastdomain.StatusPending,
NextRetryAtMS: nowMS,
CreatedAtMS: nowMS,
UpdatedAtMS: nowMS,
}
if input.DeliverAtMS > nowMS {
record.NextRetryAtMS = input.DeliverAtMS
}
created, saved, err := s.repository.SaveBroadcastOutbox(ctx, record)
if err != nil {
return broadcastdomain.PublishResult{}, err

View File

@ -50,6 +50,7 @@ type UserSocialClient interface {
DeleteFriend(ctx context.Context, req *userv1.DeleteFriendRequest) (*userv1.DeleteFriendResponse, error)
ListFriends(ctx context.Context, req *userv1.ListFriendsRequest) (*userv1.ListFriendsResponse, error)
ListFriendApplications(ctx context.Context, req *userv1.ListFriendApplicationsRequest) (*userv1.ListFriendApplicationsResponse, error)
SubmitReport(ctx context.Context, req *userv1.SubmitReportRequest) (*userv1.SubmitReportResponse, error)
}
// UserDeviceClient 抽象 gateway 对 user-service 设备 push token 的依赖。
@ -281,6 +282,10 @@ func (c *grpcUserSocialClient) ListFriendApplications(ctx context.Context, req *
return c.client.ListFriendApplications(ctx, req)
}
func (c *grpcUserSocialClient) SubmitReport(ctx context.Context, req *userv1.SubmitReportRequest) (*userv1.SubmitReportResponse, error) {
return c.client.SubmitReport(ctx, req)
}
func (c *grpcUserDeviceClient) BindPushToken(ctx context.Context, req *userv1.BindPushTokenRequest) (*userv1.BindPushTokenResponse, error) {
return c.client.BindPushToken(ctx, req)
}

View File

@ -84,6 +84,7 @@ type UserHandlers struct {
ListMyResources http.HandlerFunc
EquipMyResource http.HandlerFunc
UserSocialAction http.HandlerFunc
SubmitReport http.HandlerFunc
}
type ManagerHandlers struct {
@ -175,6 +176,14 @@ type WalletHandlers struct {
CreateRoomRedPacket http.HandlerFunc
GetRedPacket http.HandlerFunc
ClaimRedPacket http.HandlerFunc
GetCurrentRegionIMGroup http.HandlerFunc
GetRedPacketIMGroup http.HandlerFunc
SendVoiceRoomRedPacket http.HandlerFunc
ListActiveRedPackets http.HandlerFunc
ClaimVoiceRoomRedPacket http.HandlerFunc
GetVoiceRoomRedPacket http.HandlerFunc
ListRedPacketClaims http.HandlerFunc
ListSentRedPackets http.HandlerFunc
}
type VIPHandlers struct {
@ -294,6 +303,7 @@ func (r routes) registerUserRoutes() {
r.profile("/users/me", "", h.GetMyProfile)
r.profile("/users/me/resources", "", h.ListMyResources)
r.profile("/users/me/resources/{resource_id}/equip", "", h.EquipMyResource)
r.profile("/reports", http.MethodPost, h.SubmitReport)
r.profile("/users/{user_id}/{action...}", "", h.UserSocialAction)
}
@ -394,6 +404,15 @@ func (r routes) registerWalletRoutes() {
r.profile("/rooms/{room_id}/red-packets/create", http.MethodPost, h.CreateRoomRedPacket)
r.profile("/red-packets/{packet_id}", http.MethodGet, h.GetRedPacket)
r.profile("/red-packets/{packet_id}/claim", http.MethodPost, h.ClaimRedPacket)
r.profile("/app/region-im-group/current", http.MethodGet, h.GetCurrentRegionIMGroup)
r.profile("/app/voice-room/red-packet/im-group", http.MethodGet, h.GetRedPacketIMGroup)
r.profile("/app/voice-room/red-packet/config", http.MethodGet, h.GetRedPacketConfig)
r.profile("/app/voice-room/red-packet/send", http.MethodPost, h.SendVoiceRoomRedPacket)
r.profile("/app/voice-room/red-packet/active", http.MethodGet, h.ListActiveRedPackets)
r.profile("/app/voice-room/red-packet/claim", http.MethodPost, h.ClaimVoiceRoomRedPacket)
r.profile("/app/voice-room/red-packet/detail", http.MethodGet, h.GetVoiceRoomRedPacket)
r.profile("/app/voice-room/red-packet/claim-records", http.MethodGet, h.ListRedPacketClaims)
r.profile("/app/voice-room/red-packet/sent-records", http.MethodGet, h.ListSentRedPackets)
}
func (r routes) registerVIPRoutes() {

View File

@ -39,6 +39,7 @@ func (h *Handler) Routes(jwtVerifier *auth.Verifier) http.Handler {
})
walletAPI := walletapi.New(walletapi.Config{
WalletClient: h.walletClient,
RoomGuardClient: h.roomGuardClient,
RoomQueryClient: h.roomQueryClient,
UserIdentityClient: h.userIdentityClient,
UserProfileClient: h.userProfileClient,

View File

@ -6,6 +6,7 @@ import (
"hyapp/services/gateway-service/internal/transport/http/httpkit"
"net/http"
"net/http/httptest"
"strings"
"testing"
userv1 "hyapp.local/api/proto/user/v1"
@ -23,6 +24,7 @@ type fakeUserSocialClient struct {
lastDelete *userv1.DeleteFriendRequest
lastFriends *userv1.ListFriendsRequest
lastFriendApps *userv1.ListFriendApplicationsRequest
lastReport *userv1.SubmitReportRequest
}
func (f *fakeUserSocialClient) RecordProfileVisit(_ context.Context, req *userv1.RecordProfileVisitRequest) (*userv1.RecordProfileVisitResponse, error) {
@ -75,6 +77,26 @@ func (f *fakeUserSocialClient) ListFriendApplications(_ context.Context, req *us
return &userv1.ListFriendApplicationsResponse{Applications: []*userv1.FriendApplication{{RequesterUserId: 10002, TargetUserId: req.GetUserId(), Status: "pending", CreatedAtMs: 9000, UpdatedAtMs: 9000}}, Total: 1}, nil
}
func (f *fakeUserSocialClient) SubmitReport(_ context.Context, req *userv1.SubmitReportRequest) (*userv1.SubmitReportResponse, error) {
f.lastReport = req
targetType := "user"
if req.GetRoomId() != "" {
targetType = "room"
}
return &userv1.SubmitReportResponse{Report: &userv1.UserReport{
ReportId: "rpt-test",
ReporterUserId: req.GetReporterUserId(),
TargetType: targetType,
UserId: req.GetUserId(),
RoomId: req.GetRoomId(),
ReportType: req.GetReportType(),
Reason: req.GetReason(),
ImageUrls: req.GetImageUrls(),
Status: "submitted",
CreatedAtMs: 1778000000000,
}}, nil
}
func TestFriendApplicationFlowRoutesUseBackendThenIMOwnedReminder(t *testing.T) {
socialClient := &fakeUserSocialClient{}
handler := NewHandler(&fakeRoomClient{})
@ -104,6 +126,28 @@ func TestFriendApplicationFlowRoutesUseBackendThenIMOwnedReminder(t *testing.T)
}
}
func TestSubmitReportRoutePassesAuthenticatedReporterAndSingleTarget(t *testing.T) {
socialClient := &fakeUserSocialClient{}
handler := NewHandler(&fakeRoomClient{})
handler.SetUserSocialClient(socialClient)
router := handler.Routes(auth.NewVerifier("secret"))
request := httptest.NewRequest(http.MethodPost, "/api/v1/reports", strings.NewReader(`{"user_id":"10002","report_type":"pornography","reason":"bad","image_urls":["https://cdn.example.com/a.png"]}`))
request.Header.Set("Authorization", "Bearer "+signGatewayTokenWithProfile(t, "secret", 10001, true))
response := httptest.NewRecorder()
router.ServeHTTP(response, request)
if response.Code != http.StatusOK {
t.Fatalf("submit report status = %d body=%s", response.Code, response.Body.String())
}
if socialClient.lastReport == nil || socialClient.lastReport.GetReporterUserId() != 10001 || socialClient.lastReport.GetUserId() != 10002 || socialClient.lastReport.GetRoomId() != "" || socialClient.lastReport.GetReportType() != "pornography" {
t.Fatalf("submit report request mismatch: %+v", socialClient.lastReport)
}
var envelope httpkit.ResponseEnvelope
if err := json.Unmarshal(response.Body.Bytes(), &envelope); err != nil || envelope.Code != httpkit.CodeOK {
t.Fatalf("invalid envelope: %+v err=%v", envelope, err)
}
}
func TestSocialListRoutesPassPagingAndDirection(t *testing.T) {
socialClient := &fakeUserSocialClient{}
handler := NewHandler(&fakeRoomClient{})

View File

@ -61,6 +61,7 @@ func (h *Handler) UserHandlers() httproutes.UserHandlers {
ListMyFriendApplications: h.listMyFriendApplications,
GetMyProfile: h.getMyProfile,
UserSocialAction: h.userSocialAction,
SubmitReport: h.submitReport,
}
}

View File

@ -0,0 +1,94 @@
package userapi
import (
"encoding/json"
"net/http"
"strconv"
"strings"
userv1 "hyapp.local/api/proto/user/v1"
"hyapp/services/gateway-service/internal/auth"
"hyapp/services/gateway-service/internal/transport/http/httpkit"
)
type submitReportData struct {
ReportID string `json:"report_id"`
TargetType string `json:"target_type"`
UserID string `json:"user_id"`
RoomID string `json:"room_id"`
ReportType string `json:"report_type"`
Status string `json:"status"`
CreatedAtMs int64 `json:"created_at_ms"`
}
// submitReport 只把当前登录用户和举报目标透传到 user-service举报发起人不能来自客户端 body。
func (h *Handler) submitReport(writer http.ResponseWriter, request *http.Request) {
if h.userSocialClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
var body struct {
UserID json.RawMessage `json:"user_id"`
RoomID string `json:"room_id"`
ReportType string `json:"report_type"`
Reason string `json:"reason"`
ImageURLs []string `json:"image_urls"`
}
if !httpkit.Decode(writer, request, &body) {
return
}
userID, ok := optionalReportUserID(body.UserID)
if !ok {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
resp, err := h.userSocialClient.SubmitReport(request.Context(), &userv1.SubmitReportRequest{
Meta: httpkit.UserMeta(request, ""),
ReporterUserId: auth.UserIDFromContext(request.Context()),
UserId: userID,
RoomId: body.RoomID,
ReportType: body.ReportType,
Reason: body.Reason,
ImageUrls: body.ImageURLs,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, reportData(resp.GetReport()))
}
func optionalReportUserID(raw json.RawMessage) (int64, bool) {
if len(raw) == 0 || string(raw) == "null" {
return 0, true
}
var text string
if err := json.Unmarshal(raw, &text); err == nil {
text = strings.TrimSpace(text)
if text == "" {
return 0, true
}
value, err := strconv.ParseInt(text, 10, 64)
return value, err == nil && value > 0
}
var value int64
if err := json.Unmarshal(raw, &value); err != nil {
return 0, false
}
return value, value > 0
}
func reportData(report *userv1.UserReport) submitReportData {
if report == nil {
return submitReportData{}
}
return submitReportData{
ReportID: report.GetReportId(),
TargetType: report.GetTargetType(),
UserID: httpkit.UserIDString(report.GetUserId()),
RoomID: report.GetRoomId(),
ReportType: report.GetReportType(),
Status: report.GetStatus(),
CreatedAtMs: report.GetCreatedAtMs(),
}
}

View File

@ -11,6 +11,7 @@ import (
// It keeps wallet semantics behind wallet-service and only handles HTTP protocol conversion.
type Handler struct {
walletClient client.WalletClient
roomGuardClient client.RoomGuardClient
roomQueryClient client.RoomQueryClient
userIdentityClient client.UserIdentityClient
userProfileClient client.UserProfileClient
@ -19,6 +20,7 @@ type Handler struct {
type Config struct {
WalletClient client.WalletClient
RoomGuardClient client.RoomGuardClient
RoomQueryClient client.RoomQueryClient
UserIdentityClient client.UserIdentityClient
UserProfileClient client.UserProfileClient
@ -28,6 +30,7 @@ type Config struct {
func New(config Config) *Handler {
return &Handler{
walletClient: config.WalletClient,
roomGuardClient: config.RoomGuardClient,
roomQueryClient: config.RoomQueryClient,
userIdentityClient: config.UserIdentityClient,
userProfileClient: config.UserProfileClient,
@ -52,6 +55,14 @@ func (h *Handler) WalletHandlers() httproutes.WalletHandlers {
CreateRoomRedPacket: h.createRoomRedPacket,
GetRedPacket: h.getRedPacket,
ClaimRedPacket: h.claimRedPacket,
GetCurrentRegionIMGroup: h.getCurrentRegionIMGroup,
GetRedPacketIMGroup: h.getCurrentRegionIMGroup,
SendVoiceRoomRedPacket: h.sendVoiceRoomRedPacket,
ListActiveRedPackets: h.listActiveRedPackets,
ClaimVoiceRoomRedPacket: h.claimVoiceRoomRedPacket,
GetVoiceRoomRedPacket: h.getVoiceRoomRedPacket,
ListRedPacketClaims: h.listRedPacketClaims,
ListSentRedPackets: h.listSentRedPackets,
}
}

View File

@ -1,14 +1,18 @@
package walletapi
import (
"fmt"
"net/http"
"strconv"
"strings"
roomv1 "hyapp.local/api/proto/room/v1"
userv1 "hyapp.local/api/proto/user/v1"
walletv1 "hyapp.local/api/proto/wallet/v1"
"hyapp/pkg/appcode"
"hyapp/pkg/imgroup"
"hyapp/pkg/roomid"
"hyapp/pkg/xerr"
"hyapp/services/gateway-service/internal/auth"
"hyapp/services/gateway-service/internal/transport/http/httpkit"
)
@ -22,6 +26,7 @@ type redPacketConfigData struct {
ExpireSeconds int32 `json:"expire_seconds"`
DailySendLimit int32 `json:"daily_send_limit"`
UpdatedByAdminID int64 `json:"updated_by_admin_id"`
RuleURL string `json:"rule_url"`
CreatedAtMS int64 `json:"created_at_ms"`
UpdatedAtMS int64 `json:"updated_at_ms"`
}
@ -113,6 +118,377 @@ func (b claimRedPacketRequestBody) normalizedCommandID() string {
return strings.TrimSpace(b.CommandIDCamel)
}
type voiceRoomRedPacketSendBody struct {
RoomID string `json:"roomId"`
RoomIDSnake string `json:"room_id"`
PacketMode string `json:"packetMode"`
PacketType string `json:"packet_type"`
TotalAmount int64 `json:"totalAmount"`
TotalAmountV2 int64 `json:"total_amount"`
TotalCount int32 `json:"totalCount"`
PacketCount int32 `json:"packet_count"`
RequestID string `json:"requestId"`
RequestIDV2 string `json:"request_id"`
}
func (b voiceRoomRedPacketSendBody) roomID() string {
if value := strings.TrimSpace(b.RoomID); value != "" {
return value
}
return strings.TrimSpace(b.RoomIDSnake)
}
func (b voiceRoomRedPacketSendBody) mode() string {
if value := strings.TrimSpace(b.PacketMode); value != "" {
return value
}
return strings.TrimSpace(b.PacketType)
}
func (b voiceRoomRedPacketSendBody) amount() int64 {
if b.TotalAmount > 0 {
return b.TotalAmount
}
return b.TotalAmountV2
}
func (b voiceRoomRedPacketSendBody) count() int32 {
if b.TotalCount > 0 {
return b.TotalCount
}
return b.PacketCount
}
func (b voiceRoomRedPacketSendBody) requestID() string {
if value := strings.TrimSpace(b.RequestID); value != "" {
return value
}
return strings.TrimSpace(b.RequestIDV2)
}
type voiceRoomRedPacketClaimBody struct {
PacketNo string `json:"packetNo"`
PacketID string `json:"packet_id"`
RequestID string `json:"requestId"`
}
func (b voiceRoomRedPacketClaimBody) packetNo() string {
if value := strings.TrimSpace(b.PacketNo); value != "" {
return value
}
return strings.TrimSpace(b.PacketID)
}
func (h *Handler) getCurrentRegionIMGroup(writer http.ResponseWriter, request *http.Request) {
if h.userProfileClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
user, ok := h.currentUser(writer, request)
if !ok {
return
}
if user.GetRegionId() <= 0 {
httpkit.WriteError(writer, request, http.StatusConflict, "voice_room_region_missing", "conflict")
return
}
groupID, err := imgroup.RegionBroadcastGroupID(appcode.FromContext(request.Context()), user.GetRegionId())
if err != nil {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
httpkit.WriteOK(writer, request, map[string]any{
"regionId": user.GetRegionId(),
"regionCode": user.GetRegionCode(),
"regionName": user.GetRegionName(),
"groupId": groupID,
"groupName": groupID,
})
}
func (h *Handler) sendVoiceRoomRedPacket(writer http.ResponseWriter, request *http.Request) {
if h.walletClient == nil || h.roomQueryClient == nil || h.userProfileClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
var body voiceRoomRedPacketSendBody
if !httpkit.Decode(writer, request, &body) {
return
}
roomID := body.roomID()
packetType := internalRedPacketType(body.mode())
requestID := body.requestID()
if !roomid.ValidStringID(roomID) || packetType == "" || body.amount() <= 0 || body.count() <= 0 || requestID == "" {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
user, ok := h.currentUser(writer, request)
if !ok {
return
}
if user.GetRegionId() <= 0 {
httpkit.WriteError(writer, request, http.StatusConflict, "voice_room_region_missing", "conflict")
return
}
snapshotResp, err := h.roomQueryClient.GetRoomSnapshot(request.Context(), &roomv1.GetRoomSnapshotRequest{
Meta: httpkit.RoomMeta(request, roomID, ""),
RoomId: roomID,
ViewerUserId: auth.UserIDFromContext(request.Context()),
})
if err != nil {
writeRedPacketRPCError(writer, request, err)
return
}
regionID := snapshotResp.GetRoom().GetVisibleRegionId()
if regionID <= 0 {
httpkit.WriteError(writer, request, http.StatusConflict, "voice_room_region_missing", "conflict")
return
}
if regionID != user.GetRegionId() {
httpkit.WriteError(writer, request, http.StatusForbidden, "voice_room_region_not_match", "permission denied")
return
}
commandID := fmt.Sprintf("red_packet_send:%d:%s", auth.UserIDFromContext(request.Context()), requestID)
resp, err := h.walletClient.CreateRedPacket(request.Context(), &walletv1.CreateRedPacketRequest{
RequestId: requestID,
AppCode: appcode.FromContext(request.Context()),
CommandId: commandID,
SenderUserId: auth.UserIDFromContext(request.Context()),
RoomId: roomID,
RegionId: regionID,
PacketType: packetType,
TotalAmount: body.amount(),
PacketCount: body.count(),
})
if err != nil {
writeRedPacketRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, map[string]any{
"packet": voiceRoomPacketFromProto(resp.GetPacket()),
"balanceAfter": resp.GetBalanceAfter(),
"serverTime": resp.GetServerTimeMs(),
})
}
func (h *Handler) listActiveRedPackets(writer http.ResponseWriter, request *http.Request) {
if h.walletClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
roomID := strings.TrimSpace(request.URL.Query().Get("roomId"))
if roomID == "" {
roomID = strings.TrimSpace(request.URL.Query().Get("room_id"))
}
if !roomid.ValidStringID(roomID) {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
resp, err := h.walletClient.ListRedPackets(request.Context(), &walletv1.ListRedPacketsRequest{
AppCode: appcode.FromContext(request.Context()),
RoomId: roomID,
ViewerUserId: auth.UserIDFromContext(request.Context()),
Page: 1,
PageSize: 100,
})
if err != nil {
writeRedPacketRPCError(writer, request, err)
return
}
packets := make([]map[string]any, 0, len(resp.GetPackets()))
for _, packet := range resp.GetPackets() {
if packet.GetStatus() == "active" || packet.GetStatus() == "waiting_open" {
packets = append(packets, voiceRoomPacketFromProto(packet))
}
}
httpkit.WriteOK(writer, request, map[string]any{
"packets": packets,
"serverTime": resp.GetServerTimeMs(),
})
}
func (h *Handler) claimVoiceRoomRedPacket(writer http.ResponseWriter, request *http.Request) {
if h.walletClient == nil || h.roomGuardClient == nil || h.userProfileClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
var body voiceRoomRedPacketClaimBody
if !httpkit.Decode(writer, request, &body) {
return
}
packetNo := body.packetNo()
if packetNo == "" {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
packetResp, err := h.walletClient.GetRedPacket(request.Context(), &walletv1.GetRedPacketRequest{
AppCode: appcode.FromContext(request.Context()),
PacketId: packetNo,
ViewerUserId: auth.UserIDFromContext(request.Context()),
})
if err != nil {
writeRedPacketRPCError(writer, request, err)
return
}
packet := packetResp.GetPacket()
if !h.verifyRedPacketRoomPresence(writer, request, packet.GetRoomId()) {
return
}
user, ok := h.currentUser(writer, request)
if !ok {
return
}
if user.GetRegionId() <= 0 {
httpkit.WriteError(writer, request, http.StatusConflict, "voice_room_region_missing", "conflict")
return
}
if packet.GetRegionId() > 0 && packet.GetRegionId() != user.GetRegionId() {
httpkit.WriteError(writer, request, http.StatusForbidden, "voice_room_region_not_match", "permission denied")
return
}
commandID := fmt.Sprintf("red_packet_claim:%s:%d", packetNo, auth.UserIDFromContext(request.Context()))
resp, err := h.walletClient.ClaimRedPacket(request.Context(), &walletv1.ClaimRedPacketRequest{
AppCode: appcode.FromContext(request.Context()),
CommandId: commandID,
PacketId: packetNo,
UserId: auth.UserIDFromContext(request.Context()),
})
if err != nil {
writeRedPacketRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, map[string]any{
"claim": voiceRoomClaimFromProto(resp.GetClaim()),
"packet": voiceRoomPacketFromProto(resp.GetPacket()),
"balanceAfter": resp.GetBalanceAfter(),
"serverTime": resp.GetServerTimeMs(),
})
}
func (h *Handler) getVoiceRoomRedPacket(writer http.ResponseWriter, request *http.Request) {
if h.walletClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
packetNo := strings.TrimSpace(request.URL.Query().Get("packetNo"))
if packetNo == "" {
packetNo = strings.TrimSpace(request.URL.Query().Get("packet_id"))
}
if packetNo == "" {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
resp, err := h.walletClient.GetRedPacket(request.Context(), &walletv1.GetRedPacketRequest{
AppCode: appcode.FromContext(request.Context()),
PacketId: packetNo,
ViewerUserId: auth.UserIDFromContext(request.Context()),
IncludeClaims: true,
})
if err != nil {
writeRedPacketRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, map[string]any{
"packet": voiceRoomPacketFromProto(resp.GetPacket()),
"serverTime": resp.GetServerTimeMs(),
})
}
func (h *Handler) listRedPacketClaims(writer http.ResponseWriter, request *http.Request) {
packetNo := strings.TrimSpace(request.URL.Query().Get("packetNo"))
if packetNo == "" {
packetNo = strings.TrimSpace(request.URL.Query().Get("packet_id"))
}
if packetNo == "" {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
page, ok := httpkit.PositiveInt32Query(request, "page", 1)
if !ok {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
pageSize, ok := httpkit.PositiveInt32Query(request, "pageSize", 20)
if !ok {
pageSize, ok = httpkit.PositiveInt32Query(request, "page_size", 20)
}
if !ok {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
resp, err := h.walletClient.GetRedPacket(request.Context(), &walletv1.GetRedPacketRequest{
AppCode: appcode.FromContext(request.Context()),
PacketId: packetNo,
ViewerUserId: auth.UserIDFromContext(request.Context()),
IncludeClaims: true,
})
if err != nil {
writeRedPacketRPCError(writer, request, err)
return
}
all := resp.GetPacket().GetClaims()
start := int((page - 1) * pageSize)
end := start + int(pageSize)
if start > len(all) {
start = len(all)
}
if end > len(all) {
end = len(all)
}
items := make([]map[string]any, 0, end-start)
for _, claim := range all[start:end] {
items = append(items, voiceRoomClaimFromProto(claim))
}
httpkit.WriteOK(writer, request, map[string]any{
"items": items,
"total": len(all),
"page": page,
"pageSize": pageSize,
"serverTime": resp.GetServerTimeMs(),
})
}
func (h *Handler) listSentRedPackets(writer http.ResponseWriter, request *http.Request) {
if h.walletClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
page, ok := httpkit.PositiveInt32Query(request, "page", 1)
if !ok {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
pageSize, ok := httpkit.PositiveInt32Query(request, "pageSize", 20)
if !ok {
pageSize, ok = httpkit.PositiveInt32Query(request, "page_size", 20)
}
if !ok {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
resp, err := h.walletClient.ListRedPackets(request.Context(), &walletv1.ListRedPacketsRequest{
AppCode: appcode.FromContext(request.Context()),
SenderUserId: auth.UserIDFromContext(request.Context()),
Page: page,
PageSize: pageSize,
})
if err != nil {
writeRedPacketRPCError(writer, request, err)
return
}
items := make([]map[string]any, 0, len(resp.GetPackets()))
for _, packet := range resp.GetPackets() {
items = append(items, voiceRoomPacketFromProto(packet))
}
httpkit.WriteOK(writer, request, map[string]any{
"items": items,
"total": resp.GetTotal(),
"page": page,
"pageSize": pageSize,
"serverTime": resp.GetServerTimeMs(),
})
}
func (h *Handler) getRedPacketConfig(writer http.ResponseWriter, request *http.Request) {
if h.walletClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
@ -305,6 +681,7 @@ func redPacketConfigFromProto(config *walletv1.RedPacketConfig) redPacketConfigD
ExpireSeconds: config.GetExpireSeconds(),
DailySendLimit: config.GetDailySendLimit(),
UpdatedByAdminID: config.GetUpdatedByAdminId(),
RuleURL: config.GetRuleUrl(),
CreatedAtMS: config.GetCreatedAtMs(),
UpdatedAtMS: config.GetUpdatedAtMs(),
}
@ -376,3 +753,165 @@ func boolQuery(request *http.Request, name string) bool {
parsed, err := strconv.ParseBool(value)
return err == nil && parsed
}
func (h *Handler) currentUser(writer http.ResponseWriter, request *http.Request) (*userv1.User, bool) {
if h.userProfileClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return nil, false
}
resp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
Meta: httpkit.UserMeta(request, ""),
UserId: auth.UserIDFromContext(request.Context()),
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return nil, false
}
user := resp.GetUser()
if user == nil {
httpkit.WriteError(writer, request, http.StatusInternalServerError, httpkit.CodeInternalError, "internal error")
return nil, false
}
return user, true
}
func (h *Handler) verifyRedPacketRoomPresence(writer http.ResponseWriter, request *http.Request, roomID string) bool {
if !roomid.ValidStringID(roomID) {
httpkit.WriteError(writer, request, http.StatusConflict, "voice_room_red_packet_room_presence_required", "conflict")
return false
}
resp, err := h.roomGuardClient.VerifyRoomPresence(request.Context(), &roomv1.VerifyRoomPresenceRequest{
RoomId: roomID,
UserId: auth.UserIDFromContext(request.Context()),
RequestId: httpkit.RequestIDFromContext(request.Context()),
AppCode: appcode.FromContext(request.Context()),
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return false
}
if !resp.GetPresent() {
httpkit.WriteError(writer, request, http.StatusConflict, "voice_room_red_packet_room_presence_required", "conflict")
return false
}
return true
}
func writeRedPacketRPCError(writer http.ResponseWriter, request *http.Request, err error) {
if err == nil {
return
}
reason := xerr.ReasonFromGRPC(err)
statusCode, code, message := httpkit.MapReasonToHTTP(reason)
switch reason {
case xerr.RedPacketDisabled:
code = "voice_room_red_packet_disabled"
case xerr.RedPacketInvalidAmountTier, xerr.RedPacketAmountTooSmall:
code = "voice_room_red_packet_amount_invalid"
case xerr.RedPacketInvalidCountTier:
code = "voice_room_red_packet_count_invalid"
case xerr.RedPacketDailyLimitReached:
code = "voice_room_red_packet_daily_limit_exceeded"
case xerr.InsufficientBalance:
code = "voice_room_red_packet_insufficient_balance"
case xerr.NotFound:
code = "voice_room_red_packet_not_found"
case xerr.RedPacketNotOpen:
code = "voice_room_red_packet_not_started"
case xerr.RedPacketExpired:
code = "voice_room_red_packet_expired"
case xerr.RedPacketSoldOut:
code = "voice_room_red_packet_finished"
case xerr.LedgerConflict:
code = "claim_processing"
}
httpkit.WriteError(writer, request, statusCode, code, message)
}
func internalRedPacketType(mode string) string {
switch strings.ToUpper(strings.TrimSpace(mode)) {
case "IMMEDIATE":
return "normal"
case "DELAYED":
return "delayed"
}
switch strings.ToLower(strings.TrimSpace(mode)) {
case "normal":
return "normal"
case "delayed":
return "delayed"
default:
return ""
}
}
func publicRedPacketMode(packetType string) string {
switch strings.ToLower(strings.TrimSpace(packetType)) {
case "normal":
return "IMMEDIATE"
case "delayed":
return "DELAYED"
default:
return strings.ToUpper(strings.TrimSpace(packetType))
}
}
func publicRedPacketStatus(status string) string {
switch strings.ToLower(strings.TrimSpace(status)) {
case "waiting_open":
return "WAITING_OPEN"
case "active":
return "ACTIVE"
case "finished":
return "FINISHED"
case "refunded":
return "REFUNDED"
default:
return strings.ToUpper(strings.TrimSpace(status))
}
}
func voiceRoomPacketFromProto(packet *walletv1.RedPacket) map[string]any {
if packet == nil {
return map[string]any{}
}
claims := make([]map[string]any, 0, len(packet.GetClaims()))
for _, claim := range packet.GetClaims() {
claims = append(claims, voiceRoomClaimFromProto(claim))
}
return map[string]any{
"packetNo": packet.GetPacketId(),
"roomId": packet.GetRoomId(),
"regionId": packet.GetRegionId(),
"senderUserId": packet.GetSenderUserId(),
"packetMode": publicRedPacketMode(packet.GetPacketType()),
"totalAmount": packet.GetTotalAmount(),
"totalCount": packet.GetPacketCount(),
"remainingAmount": packet.GetRemainingAmount(),
"remainingCount": packet.GetRemainingCount(),
"status": publicRedPacketStatus(packet.GetStatus()),
"claimStartTime": packet.GetOpenAtMs(),
"expireTime": packet.GetExpiresAtMs(),
"createdAt": packet.GetCreatedAtMs(),
"claimedByMe": packet.GetClaimedByViewer(),
"myClaimAmount": packet.GetViewerClaimAmount(),
"refundAmount": packet.GetRefundAmount(),
"refundStatus": packet.GetRefundStatus(),
"refundedAt": packet.GetRefundedAtMs(),
"claims": claims,
}
}
func voiceRoomClaimFromProto(claim *walletv1.RedPacketClaim) map[string]any {
if claim == nil {
return map[string]any{}
}
return map[string]any{
"claimId": claim.GetClaimId(),
"packetNo": claim.GetPacketId(),
"userId": claim.GetUserId(),
"amount": claim.GetAmount(),
"status": claim.GetStatus(),
"createdAt": claim.GetCreatedAtMs(),
}
}

View File

@ -146,6 +146,28 @@ CREATE TABLE IF NOT EXISTS user_status_change_logs (
KEY idx_user_status_logs_request (app_code, request_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户状态变更日志表';
CREATE TABLE IF NOT EXISTS user_reports (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
report_id VARCHAR(96) NOT NULL COMMENT '举报单业务 ID',
reporter_user_id BIGINT NOT NULL COMMENT '举报发起人用户 ID',
target_type VARCHAR(16) NOT NULL COMMENT '举报目标类型user/room',
target_user_id BIGINT NULL COMMENT '被举报用户 ID',
room_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '被举报房间 ID',
report_type VARCHAR(64) NOT NULL COMMENT '举报类型',
reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '举报理由',
image_urls_json JSON NOT NULL COMMENT '举报图片 URL 数组',
status VARCHAR(32) NOT NULL COMMENT '处理状态',
request_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '链路请求 ID用于排查问题',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',
PRIMARY KEY (app_code, report_id),
KEY idx_user_reports_reporter (app_code, reporter_user_id, created_at_ms),
KEY idx_user_reports_target_user (app_code, target_user_id, created_at_ms),
KEY idx_user_reports_room (app_code, room_id, created_at_ms),
KEY idx_user_reports_status (app_code, status, created_at_ms),
KEY idx_user_reports_request (app_code, request_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户举报表';
CREATE TABLE IF NOT EXISTS display_user_id_sequences (
app_code VARCHAR(32) NOT NULL PRIMARY KEY COMMENT '应用编码,用于多租户隔离',
next_value BIGINT NOT NULL COMMENT '下一个值',

View File

@ -192,6 +192,12 @@ const (
FriendApplicationStatusPending = "pending"
// FriendApplicationStatusAccepted 表示好友申请已同意。
FriendApplicationStatusAccepted = "accepted"
// ReportTargetUser 表示举报目标是单个用户。
ReportTargetUser = "user"
// ReportTargetRoom 表示举报目标是单个房间。
ReportTargetRoom = "room"
// ReportStatusSubmitted 表示举报单已提交,等待后台治理流程处理。
ReportStatusSubmitted = "submitted"
)
// ProfileVisitRecord 是访问某个用户主页的去重记录。
@ -225,6 +231,36 @@ type FriendApplication struct {
UpdatedAtMs int64
}
// Report 是 App 用户提交的治理举报事实。
type Report struct {
// AppCode 是举报所属 App后台治理只在同 App 范围内处理。
AppCode string
// ReportID 是举报单不可变业务 ID。
ReportID string
// ReporterUserID 是举报发起人,只能来自已认证 token。
ReporterUserID int64
// TargetType 区分 user/room避免靠空字段猜测举报对象。
TargetType string
// UserID 是被举报用户 ID房间举报时为 0。
UserID int64
// RoomID 是被举报房间 ID用户举报时为空。
RoomID string
// ReportType 是客户端选择的稳定举报类型枚举。
ReportType string
// Reason 是用户补充的举报理由。
Reason string
// ImageURLs 是用户上传后的举报图片 URL 快照。
ImageURLs []string
// Status 是举报单当前处理状态。
Status string
// RequestID 是提交举报时的 gateway 链路追踪 ID。
RequestID string
// CreatedAtMs 是举报创建时间UTC epoch ms。
CreatedAtMs int64
// UpdatedAtMs 是举报最后更新时间UTC epoch ms。
UpdatedAtMs int64
}
// BusinessUserLookupItem 是带业务场景权限裁剪后的用户查询结果。
// 它只包含选择目标用户所需字段,不能扩展手机号、登录身份或钱包资产。
type BusinessUserLookupItem struct {

View File

@ -105,6 +105,7 @@ type fakeModerationRepository struct {
user userdomain.User
sessionIDs []string
lastCommand UserStatusCommand
lastReport userdomain.Report
}
func (r *fakeModerationRepository) SetUserStatus(_ context.Context, command UserStatusCommand) (UserStatusPersistenceResult, error) {
@ -161,6 +162,11 @@ func (r *fakeModerationRepository) ListFriendApplications(context.Context, int64
return nil, 0, nil
}
func (r *fakeModerationRepository) SubmitReport(_ context.Context, report userdomain.Report) (userdomain.Report, error) {
r.lastReport = report
return report, nil
}
func (r *fakeModerationRepository) BusinessUserLookup(context.Context, string, bool, int32) ([]userdomain.BusinessUserLookupItem, error) {
return nil, nil
}

View File

@ -0,0 +1,117 @@
package user
import (
"context"
"net/url"
"strings"
"unicode/utf8"
"hyapp/pkg/idgen"
"hyapp/pkg/roomid"
"hyapp/pkg/xerr"
userdomain "hyapp/services/user-service/internal/domain/user"
)
const (
maxReportReasonRunes = 512
maxReportImages = 9
maxReportImageURLLen = 1024
)
var validReportTypes = map[string]struct{}{
"politics": {},
"pornography": {},
"graphic_violence": {},
"verbal_abuse": {},
"fraud": {},
"illegal_content": {},
"minors": {},
"in_person_transactions": {},
"other": {},
}
// SubmitReport 校验 App 举报目标和证据 URL并把不可变举报事实交给 user repository 持久化。
func (s *Service) SubmitReport(ctx context.Context, reporterUserID int64, userID int64, roomID string, reportType string, reason string, imageURLs []string, requestID string) (userdomain.Report, error) {
if s.userRepository == nil {
return userdomain.Report{}, xerr.New(xerr.Unavailable, "user repository is not configured")
}
reporterUserID, userID, roomID, reportType, reason, imageURLs, targetType, err := normalizeReportInput(reporterUserID, userID, roomID, reportType, reason, imageURLs)
if err != nil {
return userdomain.Report{}, err
}
nowMs := s.now().UnixMilli()
report := userdomain.Report{
ReportID: idgen.New("rpt"),
ReporterUserID: reporterUserID,
TargetType: targetType,
UserID: userID,
RoomID: roomID,
ReportType: reportType,
Reason: reason,
ImageURLs: imageURLs,
Status: userdomain.ReportStatusSubmitted,
RequestID: strings.TrimSpace(requestID),
CreatedAtMs: nowMs,
UpdatedAtMs: nowMs,
}
return s.userRepository.SubmitReport(ctx, report)
}
func normalizeReportInput(reporterUserID int64, userID int64, roomID string, reportType string, reason string, imageURLs []string) (int64, int64, string, string, string, []string, string, error) {
roomID = strings.TrimSpace(roomID)
reportType = strings.ToLower(strings.TrimSpace(reportType))
reason = strings.TrimSpace(reason)
if reporterUserID <= 0 {
return 0, 0, "", "", "", nil, "", xerr.New(xerr.InvalidArgument, "reporter_user_id is required")
}
hasUser := userID > 0
hasRoom := roomID != ""
if hasUser == hasRoom {
return 0, 0, "", "", "", nil, "", xerr.New(xerr.InvalidArgument, "user_id and room_id must be exactly one")
}
if hasUser && userID == reporterUserID {
return 0, 0, "", "", "", nil, "", xerr.New(xerr.InvalidArgument, "cannot report self")
}
targetType := userdomain.ReportTargetUser
if hasRoom {
if !roomid.ValidStringID(roomID) {
return 0, 0, "", "", "", nil, "", xerr.New(xerr.InvalidArgument, "room_id is invalid")
}
targetType = userdomain.ReportTargetRoom
}
if _, ok := validReportTypes[reportType]; !ok {
return 0, 0, "", "", "", nil, "", xerr.New(xerr.InvalidArgument, "report_type is invalid")
}
if utf8.RuneCountInString(reason) > maxReportReasonRunes {
return 0, 0, "", "", "", nil, "", xerr.New(xerr.InvalidArgument, "reason is too long")
}
normalizedImages, err := normalizeReportImageURLs(imageURLs)
if err != nil {
return 0, 0, "", "", "", nil, "", err
}
return reporterUserID, userID, roomID, reportType, reason, normalizedImages, targetType, nil
}
func normalizeReportImageURLs(imageURLs []string) ([]string, error) {
if len(imageURLs) > maxReportImages {
return nil, xerr.New(xerr.InvalidArgument, "too many report images")
}
normalized := make([]string, 0, len(imageURLs))
for _, raw := range imageURLs {
value := strings.TrimSpace(raw)
if value == "" {
continue
}
if len(value) > maxReportImageURLLen {
return nil, xerr.New(xerr.InvalidArgument, "image url is too long")
}
parsed, err := url.Parse(value)
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
return nil, xerr.New(xerr.InvalidArgument, "image url is invalid")
}
normalized = append(normalized, value)
}
return normalized, nil
}

View File

@ -0,0 +1,47 @@
package user
import (
"context"
"testing"
"time"
"hyapp/pkg/appcode"
userdomain "hyapp/services/user-service/internal/domain/user"
)
func TestSubmitReportAcceptsExactlyOneTarget(t *testing.T) {
repository := &fakeModerationRepository{}
svc := New(repository, WithClock(func() time.Time { return time.UnixMilli(1_778_000_000_000).UTC() }))
ctx := appcode.WithContext(context.Background(), "lalu")
report, err := svc.SubmitReport(ctx, 10001, 10002, "", " Pornography ", " reason ", []string{" https://cdn.example.com/report.png "}, "req-report")
if err != nil {
t.Fatalf("SubmitReport failed: %v", err)
}
if report.ReportID == "" || report.TargetType != userdomain.ReportTargetUser || report.ReportType != "pornography" || report.Reason != "reason" || len(report.ImageURLs) != 1 {
t.Fatalf("report normalization mismatch: %+v", report)
}
if repository.lastReport.ReporterUserID != 10001 || repository.lastReport.UserID != 10002 || repository.lastReport.RequestID != "req-report" || repository.lastReport.CreatedAtMs != 1_778_000_000_000 {
t.Fatalf("repository report mismatch: %+v", repository.lastReport)
}
}
func TestSubmitReportRejectsMissingOrMixedTarget(t *testing.T) {
svc := New(&fakeModerationRepository{})
ctx := appcode.WithContext(context.Background(), "lalu")
tests := []struct {
name string
userID int64
roomID string
}{
{name: "missing target"},
{name: "mixed target", userID: 10002, roomID: "room_1"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
if _, err := svc.SubmitReport(ctx, 10001, test.userID, test.roomID, "fraud", "", nil, "req-report"); err == nil {
t.Fatalf("SubmitReport must reject target combination user=%d room=%q", test.userID, test.roomID)
}
})
}
}

View File

@ -37,6 +37,8 @@ type UserRepository interface {
ListFriends(ctx context.Context, userID int64, page int32, pageSize int32, cursorUpdatedAtMS int64, cursorUserID int64) ([]userdomain.FriendRecord, int64, error)
// ListFriendApplications 分页读取好友申请。
ListFriendApplications(ctx context.Context, userID int64, direction string, page int32, pageSize int32) ([]userdomain.FriendApplication, int64, error)
// SubmitReport 持久化 App 用户提交的治理举报单。
SubmitReport(ctx context.Context, report userdomain.Report) (userdomain.Report, error)
// BusinessUserLookup 按已授权业务场景查询目标用户,返回字段必须保持最小化。
BusinessUserLookup(ctx context.Context, keyword string, numericKeyword bool, pageSize int32) ([]userdomain.BusinessUserLookupItem, error)
// BatchGetUsers 批量查询用户主状态,缺失用户不应返回占位对象。

View File

@ -0,0 +1,51 @@
package user
import (
"context"
"encoding/json"
"hyapp/pkg/appcode"
userdomain "hyapp/services/user-service/internal/domain/user"
)
// SubmitReport 持久化举报事实。用户目标会校验同 App 存在;房间目标只记录 room_id不回查 room-service 明细表。
func (r *Repository) SubmitReport(ctx context.Context, report userdomain.Report) (userdomain.Report, error) {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
return userdomain.Report{}, err
}
defer tx.Rollback()
appCode := appcode.FromContext(ctx)
if err := ensureSocialUser(ctx, tx, appCode, report.ReporterUserID); err != nil {
return userdomain.Report{}, err
}
if report.UserID > 0 {
if err := ensureSocialUser(ctx, tx, appCode, report.UserID); err != nil {
return userdomain.Report{}, err
}
}
imageURLsJSON, err := json.Marshal(report.ImageURLs)
if err != nil {
return userdomain.Report{}, err
}
if _, err := tx.ExecContext(ctx, `
INSERT INTO user_reports (
app_code, report_id, reporter_user_id, target_type, target_user_id, room_id,
report_type, reason, image_urls_json, status, request_id, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
appCode, report.ReportID, report.ReporterUserID, report.TargetType, nullableReportUserID(report.UserID), report.RoomID,
report.ReportType, report.Reason, string(imageURLsJSON), report.Status, report.RequestID, report.CreatedAtMs, report.UpdatedAtMs,
); err != nil {
return userdomain.Report{}, err
}
report.AppCode = appCode
return report, tx.Commit()
}
func nullableReportUserID(userID int64) any {
if userID <= 0 {
return nil
}
return userID
}

View File

@ -111,6 +111,10 @@ func (r *Repository) ListFriendApplications(ctx context.Context, userID int64, d
return r.Repository.UserRepository().ListFriendApplications(ctx, userID, direction, page, pageSize)
}
func (r *Repository) SubmitReport(ctx context.Context, report userdomain.Report) (userdomain.Report, error) {
return r.Repository.UserRepository().SubmitReport(ctx, report)
}
// BusinessUserLookup 让测试 wrapper 继续满足业务场景用户查询接口。
func (r *Repository) BusinessUserLookup(ctx context.Context, keyword string, numericKeyword bool, pageSize int32) ([]userdomain.BusinessUserLookupItem, error) {
return r.Repository.UserRepository().BusinessUserLookup(ctx, keyword, numericKeyword, pageSize)

View File

@ -176,6 +176,21 @@ func toProtoFriendApplication(application userdomain.FriendApplication) *userv1.
}
}
func toProtoUserReport(report userdomain.Report) *userv1.UserReport {
return &userv1.UserReport{
ReportId: report.ReportID,
ReporterUserId: report.ReporterUserID,
TargetType: report.TargetType,
UserId: report.UserID,
RoomId: report.RoomID,
ReportType: report.ReportType,
Reason: report.Reason,
ImageUrls: append([]string{}, report.ImageURLs...),
Status: report.Status,
CreatedAtMs: report.CreatedAtMs,
}
}
// toProtoCountry 把国家领域对象转换为管理 RPC 响应。
func toProtoCountry(country userdomain.Country) *userv1.Country {
return &userv1.Country{

View File

@ -408,6 +408,16 @@ func (s *Server) ListFriendApplications(ctx context.Context, req *userv1.ListFri
return resp, nil
}
// SubmitReport 记录 App 用户提交的用户或房间举报。
func (s *Server) SubmitReport(ctx context.Context, req *userv1.SubmitReportRequest) (*userv1.SubmitReportResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())
report, err := s.userSvc.SubmitReport(ctx, req.GetReporterUserId(), req.GetUserId(), req.GetRoomId(), req.GetReportType(), req.GetReason(), req.GetImageUrls(), req.GetMeta().GetRequestId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &userv1.SubmitReportResponse{Report: toProtoUserReport(report)}, nil
}
// BatchGetUsers 批量返回用户主状态,缺失用户不填充占位对象。
func (s *Server) BatchGetUsers(ctx context.Context, req *userv1.BatchGetUsersRequest) (*userv1.BatchGetUsersResponse, error) {
ctx = contextWithApp(ctx, req.GetMeta())

View File

@ -666,8 +666,9 @@ CREATE TABLE IF NOT EXISTS red_packet_configs (
app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码,用于多租户隔离',
enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否启用红包',
delayed_open_seconds INT NOT NULL DEFAULT 0 COMMENT '延迟红包打开秒数',
expire_seconds INT NOT NULL DEFAULT 300 COMMENT '红包过期秒数',
expire_seconds INT NOT NULL DEFAULT 86400 COMMENT '红包过期秒数,房内红包固定 24 小时',
daily_send_limit INT NOT NULL DEFAULT 0 COMMENT '每用户 UTC 日发送上限0 表示禁止发送',
rule_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '红包规则说明链接',
updated_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '最后更新后台用户 ID',
created_at_ms BIGINT NOT NULL COMMENT '创建时间UTC epoch ms',
updated_at_ms BIGINT NOT NULL COMMENT '更新时间UTC epoch ms',

View File

@ -80,6 +80,9 @@ const (
RedPacketStatusRefunded = "refunded"
// RedPacketClaimStatusClaimed 表示红包份额领取成功。
RedPacketClaimStatusClaimed = "claimed"
// RedPacketExpireSeconds 固定房内红包 24 小时过期退款。
RedPacketExpireSeconds int32 = 24 * 60 * 60
)
// DebitGiftCommand 是 room-service 送礼扣费的账务命令。
@ -606,6 +609,7 @@ type RedPacketConfig struct {
ExpireSeconds int32
DailySendLimit int32
UpdatedByAdminID int64
RuleURL string
CreatedAtMS int64
UpdatedAtMS int64
}
@ -631,6 +635,9 @@ type RedPacket struct {
ClaimedByViewer bool
ViewerClaimAmount int64
Claims []RedPacketClaim
RefundAmount int64
RefundStatus string
RefundedAtMS int64
}
// RedPacketClaim 是单个用户抢红包的到账事实。
@ -673,9 +680,15 @@ type RedPacketClaimCommand struct {
type RedPacketClaimReceipt struct {
Claim RedPacketClaim
Packet RedPacket
BalanceAfter int64
}
type RedPacketRefundReceipt struct {
Packet RedPacket
RefundedAmount int64
}
type RedPacketQuery struct {
AppCode string
RoomID string
@ -796,6 +809,12 @@ func NormalizeGameOpType(opType string) string {
}
func NormalizeRedPacketType(packetType string) string {
switch strings.ToUpper(strings.TrimSpace(packetType)) {
case "IMMEDIATE":
return RedPacketTypeNormal
case "DELAYED":
return RedPacketTypeDelayed
}
switch strings.ToLower(strings.TrimSpace(packetType)) {
case RedPacketTypeNormal:
return RedPacketTypeNormal

View File

@ -28,18 +28,23 @@ func (s *Service) UpdateRedPacketConfig(ctx context.Context, config ledger.RedPa
config.AppCode = appcode.Normalize(config.AppCode)
config.CountTiers = normalizeCountTiers(config.CountTiers)
config.AmountTiers = normalizeAmountTiers(config.AmountTiers)
config.RuleURL = strings.TrimSpace(config.RuleURL)
config.ExpireSeconds = ledger.RedPacketExpireSeconds
if config.UpdatedByAdminID <= 0 {
return ledger.RedPacketConfig{}, xerr.New(xerr.InvalidArgument, "operator_user_id is required")
}
if len(config.CountTiers) == 0 || len(config.AmountTiers) == 0 {
return ledger.RedPacketConfig{}, xerr.New(xerr.InvalidArgument, "red packet tiers are required")
}
if config.DelayedOpenSeconds < 0 || config.ExpireSeconds <= 0 || config.DailySendLimit < 0 {
if config.DelayedOpenSeconds < 0 || config.DailySendLimit < 0 {
return ledger.RedPacketConfig{}, xerr.New(xerr.InvalidArgument, "red packet config is invalid")
}
if config.DelayedOpenSeconds >= config.ExpireSeconds {
if config.DelayedOpenSeconds >= ledger.RedPacketExpireSeconds {
return ledger.RedPacketConfig{}, xerr.New(xerr.InvalidArgument, "red packet expire_seconds must be greater than delayed_open_seconds")
}
if len(config.CountTiers) > 0 && len(config.AmountTiers) > 0 && config.AmountTiers[0] < int64(config.CountTiers[len(config.CountTiers)-1]) {
return ledger.RedPacketConfig{}, xerr.New(xerr.InvalidArgument, "red packet amount tiers must cover count tiers")
}
ctx = appcode.WithContext(ctx, config.AppCode)
return s.repository.UpdateRedPacketConfig(ctx, config)
}
@ -127,6 +132,19 @@ func (s *Service) ExpireRedPackets(ctx context.Context, appCode string, batchSiz
return s.repository.ExpireRedPackets(ctx, appCode, batchSize)
}
func (s *Service) RetryRedPacketRefund(ctx context.Context, appCode string, packetID string, operatorUserID int64) (ledger.RedPacketRefundReceipt, error) {
if s.repository == nil {
return ledger.RedPacketRefundReceipt{}, xerr.New(xerr.Unavailable, "wallet repository is not configured")
}
packetID = strings.TrimSpace(packetID)
if packetID == "" || operatorUserID <= 0 {
return ledger.RedPacketRefundReceipt{}, xerr.New(xerr.InvalidArgument, "red packet refund retry command is incomplete")
}
appCode = appcode.Normalize(appCode)
ctx = appcode.WithContext(ctx, appCode)
return s.repository.RetryRedPacketRefund(ctx, appCode, packetID, operatorUserID)
}
func normalizeCountTiers(values []int32) []int32 {
seen := make(map[int32]bool, len(values))
out := make([]int32, 0, len(values))

View File

@ -32,6 +32,7 @@ type Repository interface {
ListRedPackets(ctx context.Context, query ledger.RedPacketQuery) ([]ledger.RedPacket, int64, error)
GetRedPacket(ctx context.Context, appCode string, packetID string, viewerUserID int64, includeClaims bool) (ledger.RedPacket, error)
ExpireRedPackets(ctx context.Context, appCode string, batchSize int32) (ledger.RedPacketExpireResult, error)
RetryRedPacketRefund(ctx context.Context, appCode string, packetID string, operatorUserID int64) (ledger.RedPacketRefundReceipt, error)
ListRechargeBills(ctx context.Context, query ledger.ListRechargeBillsQuery) ([]ledger.RechargeBill, int64, error)
GetWalletOverview(ctx context.Context, userID int64) (ledger.WalletOverview, error)
GetWalletValueSummary(ctx context.Context, userID int64) (ledger.WalletValueSummary, error)

View File

@ -58,6 +58,12 @@ type redPacketClaimMetadata struct {
PacketID string `json:"packet_id"`
UserID int64 `json:"user_id"`
Amount int64 `json:"amount"`
RoomID string `json:"room_id"`
RegionID int64 `json:"region_id"`
PacketType string `json:"packet_type"`
RemainingAmount int64 `json:"remaining_amount"`
RemainingCount int32 `json:"remaining_count"`
PacketStatus string `json:"packet_status"`
BalanceAfter int64 `json:"balance_after"`
WalletTransaction string `json:"wallet_transaction_id"`
ClaimedAtMS int64 `json:"claimed_at_ms"`
@ -69,7 +75,11 @@ type redPacketRefundMetadata struct {
CommandID string `json:"command_id"`
PacketID string `json:"packet_id"`
SenderUserID int64 `json:"sender_user_id"`
RoomID string `json:"room_id"`
RegionID int64 `json:"region_id"`
PacketType string `json:"packet_type"`
RefundAmount int64 `json:"refund_amount"`
PacketStatus string `json:"packet_status"`
BalanceAfter int64 `json:"balance_after"`
WalletTransaction string `json:"wallet_transaction_id"`
RefundedAtMS int64 `json:"refunded_at_ms"`
@ -110,21 +120,23 @@ func (r *Repository) UpdateRedPacketConfig(ctx context.Context, config ledger.Re
if _, err := tx.ExecContext(ctx, `
INSERT INTO red_packet_configs (
app_code, enabled, delayed_open_seconds, expire_seconds, daily_send_limit,
app_code, enabled, delayed_open_seconds, expire_seconds, daily_send_limit, rule_url,
updated_by_admin_id, created_at_ms, updated_at_ms
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
enabled = VALUES(enabled),
delayed_open_seconds = VALUES(delayed_open_seconds),
expire_seconds = VALUES(expire_seconds),
daily_send_limit = VALUES(daily_send_limit),
rule_url = VALUES(rule_url),
updated_by_admin_id = VALUES(updated_by_admin_id),
updated_at_ms = VALUES(updated_at_ms)`,
config.AppCode,
config.Enabled,
config.DelayedOpenSeconds,
config.ExpireSeconds,
ledger.RedPacketExpireSeconds,
config.DailySendLimit,
strings.TrimSpace(config.RuleURL),
config.UpdatedByAdminID,
nowMS,
nowMS,
@ -202,7 +214,7 @@ func (r *Repository) CreateRedPacket(ctx context.Context, command ledger.RedPack
if command.TotalAmount < int64(command.PacketCount) {
return ledger.RedPacketCreateReceipt{}, xerr.New(xerr.RedPacketAmountTooSmall, "red packet amount is too small")
}
if config.ExpireSeconds <= 0 || (command.PacketType == ledger.RedPacketTypeDelayed && config.ExpireSeconds <= config.DelayedOpenSeconds) {
if ledger.RedPacketExpireSeconds <= 0 || (command.PacketType == ledger.RedPacketTypeDelayed && ledger.RedPacketExpireSeconds <= config.DelayedOpenSeconds) {
return ledger.RedPacketCreateReceipt{}, xerr.New(xerr.InvalidArgument, "red packet config time range is invalid")
}
if err := r.incrementRedPacketDailyCounter(ctx, tx, command.SenderUserID, config.DailySendLimit, nowMS); err != nil {
@ -229,7 +241,7 @@ func (r *Repository) CreateRedPacket(ctx context.Context, command ledger.RedPack
openAtMS = nowMS + int64(config.DelayedOpenSeconds)*1000
status = ledger.RedPacketStatusWaitingOpen
}
expiresAtMS := nowMS + int64(config.ExpireSeconds)*1000
expiresAtMS := nowMS + int64(ledger.RedPacketExpireSeconds)*1000
balanceAfter := sender.AvailableAmount - command.TotalAmount
metadata := redPacketMetadata{
AppCode: command.AppCode,
@ -322,11 +334,11 @@ func (r *Repository) ClaimRedPacket(ctx context.Context, command ledger.RedPacke
if nowMS < packet.OpenAtMS {
return ledger.RedPacketClaimReceipt{}, xerr.New(xerr.RedPacketNotOpen, "red packet is not open")
}
if claimed, err := r.userClaimedRedPacket(ctx, tx, command.PacketID, command.UserID); err != nil || claimed {
if existingClaim, claimed, err := r.getUserRedPacketClaim(ctx, tx, command.PacketID, command.UserID); err != nil || claimed {
if err != nil {
return ledger.RedPacketClaimReceipt{}, err
}
return ledger.RedPacketClaimReceipt{}, xerr.New(xerr.RedPacketAlreadyClaimed, "red packet already claimed")
return ledger.RedPacketClaimReceipt{Claim: existingClaim, Packet: packet}, nil
}
allocationNo, amount, err := r.lockOneRedPacketAllocation(ctx, tx, command.PacketID)
if err != nil {
@ -350,6 +362,11 @@ func (r *Repository) ClaimRedPacket(ctx context.Context, command ledger.RedPacke
PacketID: command.PacketID,
UserID: command.UserID,
Amount: amount,
RoomID: packet.RoomID,
RegionID: packet.RegionID,
PacketType: packet.PacketType,
RemainingAmount: packet.RemainingAmount - amount,
RemainingCount: packet.RemainingCount - 1,
BalanceAfter: balanceAfter,
WalletTransaction: transactionID,
ClaimedAtMS: nowMS,
@ -376,7 +393,14 @@ func (r *Repository) ClaimRedPacket(ctx context.Context, command ledger.RedPacke
}
if err := r.insertRedPacketClaim(ctx, tx, metadata); err != nil {
if isMySQLDuplicateError(err) {
return ledger.RedPacketClaimReceipt{}, xerr.New(xerr.RedPacketAlreadyClaimed, "red packet already claimed")
existingClaim, claimed, lookupErr := r.getUserRedPacketClaim(ctx, tx, command.PacketID, command.UserID)
if lookupErr != nil {
return ledger.RedPacketClaimReceipt{}, lookupErr
}
if claimed {
return r.redPacketClaimReceiptForTransaction(ctx, tx, existingClaim.WalletTransactionID)
}
return ledger.RedPacketClaimReceipt{}, xerr.New(xerr.LedgerConflict, "red packet claim already exists")
}
return ledger.RedPacketClaimReceipt{}, err
}
@ -395,6 +419,7 @@ func (r *Repository) ClaimRedPacket(ctx context.Context, command ledger.RedPacke
if newRemainingCount == 0 {
newStatus = ledger.RedPacketStatusFinished
}
metadata.PacketStatus = newStatus
if _, err := tx.ExecContext(ctx, `
UPDATE red_packets
SET remaining_amount = ?, remaining_count = ?, status = ?, updated_at_ms = ?
@ -412,7 +437,11 @@ func (r *Repository) ClaimRedPacket(ctx context.Context, command ledger.RedPacke
if err := tx.Commit(); err != nil {
return ledger.RedPacketClaimReceipt{}, err
}
return ledger.RedPacketClaimReceipt{Claim: redPacketClaimFromMetadata(metadata), BalanceAfter: balanceAfter}, nil
packet.RemainingAmount = newRemainingAmount
packet.RemainingCount = newRemainingCount
packet.Status = newStatus
packet.UpdatedAtMS = nowMS
return ledger.RedPacketClaimReceipt{Claim: redPacketClaimFromMetadata(metadata), Packet: packet, BalanceAfter: balanceAfter}, nil
}
func (r *Repository) ListRedPackets(ctx context.Context, query ledger.RedPacketQuery) ([]ledger.RedPacket, int64, error) {
@ -458,6 +487,9 @@ func (r *Repository) ListRedPackets(ctx context.Context, query ledger.RedPacketQ
if err := rows.Err(); err != nil {
return nil, 0, err
}
if err := r.attachRefunds(ctx, packets); err != nil {
return nil, 0, err
}
if query.ViewerUserID > 0 {
if err := r.attachViewerClaims(ctx, packets, query.ViewerUserID); err != nil {
return nil, 0, err
@ -480,6 +512,15 @@ func (r *Repository) GetRedPacket(ctx context.Context, app string, packetID stri
if err := r.attachViewerClaims(ctx, packets, viewerUserID); err != nil {
return ledger.RedPacket{}, err
}
if err := r.attachRefunds(ctx, packets); err != nil {
return ledger.RedPacket{}, err
}
packet = packets[0]
} else {
packets := []ledger.RedPacket{packet}
if err := r.attachRefunds(ctx, packets); err != nil {
return ledger.RedPacket{}, err
}
packet = packets[0]
}
if includeClaims {
@ -541,6 +582,30 @@ func (r *Repository) ExpireRedPackets(ctx context.Context, app string, batchSize
return result, nil
}
func (r *Repository) RetryRedPacketRefund(ctx context.Context, app string, packetID string, operatorUserID int64) (ledger.RedPacketRefundReceipt, error) {
if r == nil || r.db == nil {
return ledger.RedPacketRefundReceipt{}, xerr.New(xerr.Unavailable, "mysql repository is not configured")
}
_ = operatorUserID
ctx = contextWithCommandApp(ctx, app)
nowMS := time.Now().UTC().UnixMilli()
refunded, amount, err := r.expireOneRedPacket(ctx, strings.TrimSpace(packetID), nowMS)
if err != nil {
return ledger.RedPacketRefundReceipt{}, err
}
packet, err := r.GetRedPacket(ctx, appcode.FromContext(ctx), packetID, 0, false)
if err != nil {
return ledger.RedPacketRefundReceipt{}, err
}
if !refunded {
if packet.Status == ledger.RedPacketStatusRefunded {
return ledger.RedPacketRefundReceipt{Packet: packet, RefundedAmount: packet.RefundAmount}, nil
}
return ledger.RedPacketRefundReceipt{}, xerr.New(xerr.RedPacketExpired, "red packet is not expired")
}
return ledger.RedPacketRefundReceipt{Packet: packet, RefundedAmount: amount}, nil
}
func (r *Repository) expireOneRedPacket(ctx context.Context, packetID string, nowMS int64) (bool, int64, error) {
tx, err := r.db.BeginTx(ctx, nil)
if err != nil {
@ -588,7 +653,11 @@ func (r *Repository) expireOneRedPacket(ctx context.Context, packetID string, no
CommandID: commandID,
PacketID: packetID,
SenderUserID: packet.SenderUserID,
RoomID: packet.RoomID,
RegionID: packet.RegionID,
PacketType: packet.PacketType,
RefundAmount: refundAmount,
PacketStatus: ledger.RedPacketStatusRefunded,
BalanceAfter: balanceAfter,
WalletTransaction: transactionID,
RefundedAtMS: nowMS,
@ -649,12 +718,12 @@ func (r *Repository) expireOneRedPacket(ctx context.Context, packetID string, no
func (r *Repository) getRedPacketConfig(ctx context.Context, querier redPacketQuerier) (ledger.RedPacketConfig, error) {
row := querier.QueryRowContext(ctx, `
SELECT app_code, enabled, delayed_open_seconds, expire_seconds, daily_send_limit,
updated_by_admin_id, created_at_ms, updated_at_ms
updated_by_admin_id, COALESCE(rule_url, ''), created_at_ms, updated_at_ms
FROM red_packet_configs
WHERE app_code = ?`,
appcode.FromContext(ctx),
)
config := ledger.RedPacketConfig{AppCode: appcode.FromContext(ctx), ExpireSeconds: 300}
config := ledger.RedPacketConfig{AppCode: appcode.FromContext(ctx), ExpireSeconds: ledger.RedPacketExpireSeconds}
if err := row.Scan(
&config.AppCode,
&config.Enabled,
@ -662,6 +731,7 @@ func (r *Repository) getRedPacketConfig(ctx context.Context, querier redPacketQu
&config.ExpireSeconds,
&config.DailySendLimit,
&config.UpdatedByAdminID,
&config.RuleURL,
&config.CreatedAtMS,
&config.UpdatedAtMS,
); err != nil {
@ -670,6 +740,7 @@ func (r *Repository) getRedPacketConfig(ctx context.Context, querier redPacketQu
}
return config, nil
}
config.ExpireSeconds = ledger.RedPacketExpireSeconds
counts, err := r.listRedPacketCountTiers(ctx, querier)
if err != nil {
return ledger.RedPacketConfig{}, err
@ -869,19 +940,32 @@ func scanRedPacket(row redPacketScanner) (ledger.RedPacket, error) {
return packet, err
}
func (r *Repository) userClaimedRedPacket(ctx context.Context, tx *sql.Tx, packetID string, userID int64) (bool, error) {
var claimID string
func (r *Repository) getUserRedPacketClaim(ctx context.Context, tx *sql.Tx, packetID string, userID int64) (ledger.RedPacketClaim, bool, error) {
var claim ledger.RedPacketClaim
err := tx.QueryRowContext(ctx, `
SELECT claim_id
SELECT app_code, claim_id, command_id, packet_id, user_id, amount, wallet_transaction_id,
status, failure_reason, created_at_ms, updated_at_ms
FROM red_packet_claims
WHERE app_code = ? AND packet_id = ? AND user_id = ?
LIMIT 1`,
appcode.FromContext(ctx), packetID, userID,
).Scan(&claimID)
).Scan(
&claim.AppCode,
&claim.ClaimID,
&claim.CommandID,
&claim.PacketID,
&claim.UserID,
&claim.Amount,
&claim.WalletTransactionID,
&claim.Status,
&claim.FailureReason,
&claim.CreatedAtMS,
&claim.UpdatedAtMS,
)
if errors.Is(err, sql.ErrNoRows) {
return false, nil
return ledger.RedPacketClaim{}, false, nil
}
return err == nil, err
return claim, err == nil, err
}
func (r *Repository) lockOneRedPacketAllocation(ctx context.Context, tx *sql.Tx, packetID string) (int32, int64, error) {
@ -944,6 +1028,50 @@ func (r *Repository) attachViewerClaims(ctx context.Context, packets []ledger.Re
return rows.Err()
}
func (r *Repository) attachRefunds(ctx context.Context, packets []ledger.RedPacket) error {
if len(packets) == 0 {
return nil
}
packetIDs := make([]string, 0, len(packets))
indexByID := make(map[string]int, len(packets))
for index := range packets {
packetIDs = append(packetIDs, packets[index].PacketID)
indexByID[packets[index].PacketID] = index
}
placeholders := strings.TrimRight(strings.Repeat("?,", len(packetIDs)), ",")
args := []any{appcode.FromContext(ctx)}
for _, packetID := range packetIDs {
args = append(args, packetID)
}
rows, err := r.db.QueryContext(ctx, `
SELECT packet_id, refund_amount, status, updated_at_ms
FROM red_packet_refunds
WHERE app_code = ? AND packet_id IN (`+placeholders+`)`,
args...,
)
if err != nil {
return err
}
defer rows.Close()
for rows.Next() {
var packetID string
var amount int64
var status string
var updatedAtMS int64
if err := rows.Scan(&packetID, &amount, &status, &updatedAtMS); err != nil {
return err
}
if index, ok := indexByID[packetID]; ok {
packets[index].RefundAmount = amount
packets[index].RefundStatus = status
if status == ledger.RedPacketStatusRefunded {
packets[index].RefundedAtMS = updatedAtMS
}
}
}
return rows.Err()
}
func (r *Repository) listRedPacketClaims(ctx context.Context, packetID string) ([]ledger.RedPacketClaim, error) {
rows, err := r.db.QueryContext(ctx, `
SELECT app_code, claim_id, command_id, packet_id, user_id, amount, wallet_transaction_id,
@ -1015,7 +1143,11 @@ func (r *Repository) redPacketClaimReceiptForTransaction(ctx context.Context, tx
if err := json.Unmarshal([]byte(raw), &metadata); err != nil {
return ledger.RedPacketClaimReceipt{}, err
}
return ledger.RedPacketClaimReceipt{Claim: redPacketClaimFromMetadata(metadata), BalanceAfter: metadata.BalanceAfter}, nil
packet, err := r.getRedPacket(ctx, tx, metadata.PacketID)
if err != nil {
return ledger.RedPacketClaimReceipt{Claim: redPacketClaimFromMetadata(metadata), BalanceAfter: metadata.BalanceAfter}, nil
}
return ledger.RedPacketClaimReceipt{Claim: redPacketClaimFromMetadata(metadata), Packet: packet, BalanceAfter: metadata.BalanceAfter}, nil
}
func redPacketFromMetadata(metadata redPacketMetadata) ledger.RedPacket {

View File

@ -33,6 +33,7 @@ func (s *Server) UpdateRedPacketConfig(ctx context.Context, req *walletv1.Update
ExpireSeconds: req.GetExpireSeconds(),
DailySendLimit: req.GetDailySendLimit(),
UpdatedByAdminID: req.GetOperatorUserId(),
RuleURL: req.GetRuleUrl(),
})
if err != nil {
return nil, xerr.ToGRPCError(err)
@ -80,6 +81,7 @@ func (s *Server) ClaimRedPacket(ctx context.Context, req *walletv1.ClaimRedPacke
Claim: redPacketClaimToProto(receipt.Claim),
BalanceAfter: receipt.BalanceAfter,
ServerTimeMs: time.Now().UTC().UnixMilli(),
Packet: redPacketToProto(receipt.Packet),
}, nil
}
@ -130,6 +132,19 @@ func (s *Server) ExpireRedPackets(ctx context.Context, req *walletv1.ExpireRedPa
}, nil
}
func (s *Server) RetryRedPacketRefund(ctx context.Context, req *walletv1.RetryRedPacketRefundRequest) (*walletv1.RetryRedPacketRefundResponse, error) {
ctx = appcode.WithContext(ctx, req.GetAppCode())
receipt, err := s.svc.RetryRedPacketRefund(ctx, req.GetAppCode(), req.GetPacketId(), req.GetOperatorUserId())
if err != nil {
return nil, xerr.ToGRPCError(err)
}
return &walletv1.RetryRedPacketRefundResponse{
Packet: redPacketToProto(receipt.Packet),
RefundedAmount: receipt.RefundedAmount,
ServerTimeMs: time.Now().UTC().UnixMilli(),
}, nil
}
func redPacketConfigToProto(config ledger.RedPacketConfig) *walletv1.RedPacketConfig {
return &walletv1.RedPacketConfig{
AppCode: config.AppCode,
@ -142,6 +157,7 @@ func redPacketConfigToProto(config ledger.RedPacketConfig) *walletv1.RedPacketCo
UpdatedByAdminId: config.UpdatedByAdminID,
CreatedAtMs: config.CreatedAtMS,
UpdatedAtMs: config.UpdatedAtMS,
RuleUrl: config.RuleURL,
}
}
@ -170,6 +186,9 @@ func redPacketToProto(packet ledger.RedPacket) *walletv1.RedPacket {
ClaimedByViewer: packet.ClaimedByViewer,
ViewerClaimAmount: packet.ViewerClaimAmount,
Claims: claims,
RefundAmount: packet.RefundAmount,
RefundStatus: packet.RefundStatus,
RefundedAtMs: packet.RefundedAtMS,
}
}