diff --git a/auto/api/v2/loose.go b/auto/api/v2/loose.go new file mode 100644 index 000000000..d81f22813 --- /dev/null +++ b/auto/api/v2/loose.go @@ -0,0 +1,185 @@ +// Code generated by go-mir. DO NOT EDIT. +// versions: +// - mir v4.2.0 + +package v2 + +import ( + "net/http" + + "github.com/alimy/mir/v4" + "github.com/gin-gonic/gin" + "github.com/rocboss/paopao-ce/internal/model/v2/web" +) + +type _binding_ interface { + Bind(*gin.Context) mir.Error +} + +type _render_ interface { + Render(*gin.Context) +} + +type _default_ interface { + Bind(*gin.Context, any) mir.Error + BindQuery(*gin.Context, any) mir.Error + Render(*gin.Context, any, mir.Error) +} + +type Loose interface { + _default_ + + // Chain provide handlers chain for gin + Chain() gin.HandlersChain + + TweetDetail(*web.TweetDetailReq) (*web.TweetDetailResp, mir.Error) + TweetComments(*web.TweetCommentsReq) (*web.TweetCommentsResp, mir.Error) + TopicList(*web.TopicListReq) (*web.TopicListResp, mir.Error) + GetUserProfile(*web.GetUserProfileReq) (*web.GetUserProfileResp, mir.Error) + GetUserTweets(*web.GetUserTweetsReq) (*web.GetUserTweetsResp, mir.Error) + Timeline(*web.TimelineReq) (*web.TimelineResp, mir.Error) + + mustEmbedUnimplementedLooseServant() +} + +// RegisterLooseServant register Loose servant to gin +func RegisterLooseServant(e *gin.Engine, s Loose) { + router := e.Group("v2") + // use chain for router + middlewares := s.Chain() + router.Use(middlewares...) + + // register routes info to router + router.Handle("GET", "post", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.TweetDetailReq) + if err := s.BindQuery(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.TweetDetail(req) + s.Render(c, resp, err) + }) + router.Handle("GET", "post/comments", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.TweetCommentsReq) + if err := s.BindQuery(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.TweetComments(req) + if err != nil { + s.Render(c, nil, err) + return + } + var rv _render_ = resp + rv.Render(c) + }) + router.Handle("GET", "tags", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.TopicListReq) + if err := s.BindQuery(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.TopicList(req) + s.Render(c, resp, err) + }) + router.Handle("GET", "user/profile", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.GetUserProfileReq) + if err := s.BindQuery(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.GetUserProfile(req) + s.Render(c, resp, err) + }) + router.Handle("GET", "user/posts", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.GetUserTweetsReq) + if err := s.BindQuery(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.GetUserTweets(req) + if err != nil { + s.Render(c, nil, err) + return + } + var rv _render_ = resp + rv.Render(c) + }) + router.Handle("GET", "posts", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.TimelineReq) + if err := s.BindQuery(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.Timeline(req) + if err != nil { + s.Render(c, nil, err) + return + } + var rv _render_ = resp + rv.Render(c) + }) +} + +// UnimplementedLooseServant can be embedded to have forward compatible implementations. +type UnimplementedLooseServant struct{} + +func (UnimplementedLooseServant) Chain() gin.HandlersChain { + return nil +} + +func (UnimplementedLooseServant) TweetDetail(req *web.TweetDetailReq) (*web.TweetDetailResp, mir.Error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedLooseServant) TweetComments(req *web.TweetCommentsReq) (*web.TweetCommentsResp, mir.Error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedLooseServant) TopicList(req *web.TopicListReq) (*web.TopicListResp, mir.Error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedLooseServant) GetUserProfile(req *web.GetUserProfileReq) (*web.GetUserProfileResp, mir.Error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedLooseServant) GetUserTweets(req *web.GetUserTweetsReq) (*web.GetUserTweetsResp, mir.Error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedLooseServant) Timeline(req *web.TimelineReq) (*web.TimelineResp, mir.Error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedLooseServant) mustEmbedUnimplementedLooseServant() {} diff --git a/auto/api/v2/pub.go b/auto/api/v2/pub.go new file mode 100644 index 000000000..37ee418ae --- /dev/null +++ b/auto/api/v2/pub.go @@ -0,0 +1,118 @@ +// Code generated by go-mir. DO NOT EDIT. +// versions: +// - mir v4.2.0 + +package v2 + +import ( + "net/http" + + "github.com/alimy/mir/v4" + "github.com/gin-gonic/gin" + "github.com/rocboss/paopao-ce/internal/model/web" +) + +type Pub interface { + _default_ + + SendCaptcha(*web.SendCaptchaReq) mir.Error + GetCaptcha() (*web.GetCaptchaResp, mir.Error) + Register(*web.RegisterReq) (*web.RegisterResp, mir.Error) + Login(*web.LoginReq) (*web.LoginResp, mir.Error) + Version() (*web.VersionResp, mir.Error) + + mustEmbedUnimplementedPubServant() +} + +// RegisterPubServant register Pub servant to gin +func RegisterPubServant(e *gin.Engine, s Pub) { + router := e.Group("v2") + + // register routes info to router + router.Handle("POST", "captcha", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.SendCaptchaReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + s.Render(c, nil, s.SendCaptcha(req)) + }) + router.Handle("GET", "captcha", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + + resp, err := s.GetCaptcha() + s.Render(c, resp, err) + }) + router.Handle("POST", "auth/register", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.RegisterReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.Register(req) + s.Render(c, resp, err) + }) + router.Handle("POST", "auth/login", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + req := new(web.LoginReq) + if err := s.Bind(c, req); err != nil { + s.Render(c, nil, err) + return + } + resp, err := s.Login(req) + s.Render(c, resp, err) + }) + router.Handle("GET", "/", func(c *gin.Context) { + select { + case <-c.Request.Context().Done(): + return + default: + } + + resp, err := s.Version() + s.Render(c, resp, err) + }) +} + +// UnimplementedPubServant can be embedded to have forward compatible implementations. +type UnimplementedPubServant struct{} + +func (UnimplementedPubServant) SendCaptcha(req *web.SendCaptchaReq) mir.Error { + return mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedPubServant) GetCaptcha() (*web.GetCaptchaResp, mir.Error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedPubServant) Register(req *web.RegisterReq) (*web.RegisterResp, mir.Error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedPubServant) Login(req *web.LoginReq) (*web.LoginResp, mir.Error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedPubServant) Version() (*web.VersionResp, mir.Error) { + return nil, mir.Errorln(http.StatusNotImplemented, http.StatusText(http.StatusNotImplemented)) +} + +func (UnimplementedPubServant) mustEmbedUnimplementedPubServant() {} diff --git a/auto/connect/core/v1/corev1connect/auth.connect.go b/auto/connect/core/v1/corev1connect/auth.connect.go index 50c3d0f80..e43626b51 100644 --- a/auto/connect/core/v1/corev1connect/auth.connect.go +++ b/auto/connect/core/v1/corev1connect/auth.connect.go @@ -44,14 +44,6 @@ const ( AuthenticateServiceLogoutProcedure = "/core.v1.AuthenticateService/logout" ) -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - authenticateServiceServiceDescriptor = v1.File_core_v1_auth_proto.Services().ByName("AuthenticateService") - authenticateServicePreLoginMethodDescriptor = authenticateServiceServiceDescriptor.Methods().ByName("preLogin") - authenticateServiceLoginMethodDescriptor = authenticateServiceServiceDescriptor.Methods().ByName("login") - authenticateServiceLogoutMethodDescriptor = authenticateServiceServiceDescriptor.Methods().ByName("logout") -) - // AuthenticateServiceClient is a client for the core.v1.AuthenticateService service. type AuthenticateServiceClient interface { PreLogin(context.Context, *connect.Request[v1.User]) (*connect.Response[v1.ActionReply], error) @@ -68,23 +60,24 @@ type AuthenticateServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewAuthenticateServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) AuthenticateServiceClient { baseURL = strings.TrimRight(baseURL, "/") + authenticateServiceMethods := v1.File_core_v1_auth_proto.Services().ByName("AuthenticateService").Methods() return &authenticateServiceClient{ preLogin: connect.NewClient[v1.User, v1.ActionReply]( httpClient, baseURL+AuthenticateServicePreLoginProcedure, - connect.WithSchema(authenticateServicePreLoginMethodDescriptor), + connect.WithSchema(authenticateServiceMethods.ByName("preLogin")), connect.WithClientOptions(opts...), ), login: connect.NewClient[v1.User, v1.LoginReply]( httpClient, baseURL+AuthenticateServiceLoginProcedure, - connect.WithSchema(authenticateServiceLoginMethodDescriptor), + connect.WithSchema(authenticateServiceMethods.ByName("login")), connect.WithClientOptions(opts...), ), logout: connect.NewClient[v1.User, v1.ActionReply]( httpClient, baseURL+AuthenticateServiceLogoutProcedure, - connect.WithSchema(authenticateServiceLogoutMethodDescriptor), + connect.WithSchema(authenticateServiceMethods.ByName("logout")), connect.WithClientOptions(opts...), ), } @@ -125,22 +118,23 @@ type AuthenticateServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewAuthenticateServiceHandler(svc AuthenticateServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + authenticateServiceMethods := v1.File_core_v1_auth_proto.Services().ByName("AuthenticateService").Methods() authenticateServicePreLoginHandler := connect.NewUnaryHandler( AuthenticateServicePreLoginProcedure, svc.PreLogin, - connect.WithSchema(authenticateServicePreLoginMethodDescriptor), + connect.WithSchema(authenticateServiceMethods.ByName("preLogin")), connect.WithHandlerOptions(opts...), ) authenticateServiceLoginHandler := connect.NewUnaryHandler( AuthenticateServiceLoginProcedure, svc.Login, - connect.WithSchema(authenticateServiceLoginMethodDescriptor), + connect.WithSchema(authenticateServiceMethods.ByName("login")), connect.WithHandlerOptions(opts...), ) authenticateServiceLogoutHandler := connect.NewUnaryHandler( AuthenticateServiceLogoutProcedure, svc.Logout, - connect.WithSchema(authenticateServiceLogoutMethodDescriptor), + connect.WithSchema(authenticateServiceMethods.ByName("logout")), connect.WithHandlerOptions(opts...), ) return "/core.v1.AuthenticateService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/auto/connect/greet/v1/greetv1connect/greet.connect.go b/auto/connect/greet/v1/greetv1connect/greet.connect.go index 59d26952b..02140909c 100644 --- a/auto/connect/greet/v1/greetv1connect/greet.connect.go +++ b/auto/connect/greet/v1/greetv1connect/greet.connect.go @@ -37,12 +37,6 @@ const ( GreetServiceGreetProcedure = "/greet.v1.GreetService/Greet" ) -// These variables are the protoreflect.Descriptor objects for the RPCs defined in this package. -var ( - greetServiceServiceDescriptor = v1.File_greet_v1_greet_proto.Services().ByName("GreetService") - greetServiceGreetMethodDescriptor = greetServiceServiceDescriptor.Methods().ByName("Greet") -) - // GreetServiceClient is a client for the greet.v1.GreetService service. type GreetServiceClient interface { Greet(context.Context, *connect.Request[v1.GreetRequest]) (*connect.Response[v1.GreetResponse], error) @@ -57,11 +51,12 @@ type GreetServiceClient interface { // http://api.acme.com or https://acme.com/grpc). func NewGreetServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) GreetServiceClient { baseURL = strings.TrimRight(baseURL, "/") + greetServiceMethods := v1.File_greet_v1_greet_proto.Services().ByName("GreetService").Methods() return &greetServiceClient{ greet: connect.NewClient[v1.GreetRequest, v1.GreetResponse]( httpClient, baseURL+GreetServiceGreetProcedure, - connect.WithSchema(greetServiceGreetMethodDescriptor), + connect.WithSchema(greetServiceMethods.ByName("Greet")), connect.WithClientOptions(opts...), ), } @@ -88,10 +83,11 @@ type GreetServiceHandler interface { // By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf // and JSON codecs. They also support gzip compression. func NewGreetServiceHandler(svc GreetServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + greetServiceMethods := v1.File_greet_v1_greet_proto.Services().ByName("GreetService").Methods() greetServiceGreetHandler := connect.NewUnaryHandler( GreetServiceGreetProcedure, svc.Greet, - connect.WithSchema(greetServiceGreetMethodDescriptor), + connect.WithSchema(greetServiceMethods.ByName("Greet")), connect.WithHandlerOptions(opts...), ) return "/greet.v1.GreetService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/auto/rpc/core/v1/auth.pb.go b/auto/rpc/core/v1/auth.pb.go index d319130c1..0c16ea321 100644 --- a/auto/rpc/core/v1/auth.pb.go +++ b/auto/rpc/core/v1/auth.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.36.3 // protoc (unknown) // source: core/v1/auth.proto @@ -21,20 +21,17 @@ const ( ) type User struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + PhoneNum string `protobuf:"bytes,1,opt,name=phone_num,json=phoneNum,proto3" json:"phone_num,omitempty"` unknownFields protoimpl.UnknownFields - - PhoneNum string `protobuf:"bytes,1,opt,name=phone_num,json=phoneNum,proto3" json:"phone_num,omitempty"` + sizeCache protoimpl.SizeCache } func (x *User) Reset() { *x = User{} - if protoimpl.UnsafeEnabled { - mi := &file_core_v1_auth_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_v1_auth_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *User) String() string { @@ -45,7 +42,7 @@ func (*User) ProtoMessage() {} func (x *User) ProtoReflect() protoreflect.Message { mi := &file_core_v1_auth_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -68,21 +65,18 @@ func (x *User) GetPhoneNum() string { } type UserVerify struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - PhoneNum string `protobuf:"bytes,1,opt,name=phone_num,json=phoneNum,proto3" json:"phone_num,omitempty"` - VerificationCode string `protobuf:"bytes,2,opt,name=verification_code,json=verificationCode,proto3" json:"verification_code,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + PhoneNum string `protobuf:"bytes,1,opt,name=phone_num,json=phoneNum,proto3" json:"phone_num,omitempty"` + VerificationCode string `protobuf:"bytes,2,opt,name=verification_code,json=verificationCode,proto3" json:"verification_code,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *UserVerify) Reset() { *x = UserVerify{} - if protoimpl.UnsafeEnabled { - mi := &file_core_v1_auth_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_v1_auth_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *UserVerify) String() string { @@ -93,7 +87,7 @@ func (*UserVerify) ProtoMessage() {} func (x *UserVerify) ProtoReflect() protoreflect.Message { mi := &file_core_v1_auth_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -123,21 +117,18 @@ func (x *UserVerify) GetVerificationCode() string { } type LoginReply struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + StatusCode int32 `protobuf:"varint,1,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` + Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` unknownFields protoimpl.UnknownFields - - StatusCode int32 `protobuf:"varint,1,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` - Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"` + sizeCache protoimpl.SizeCache } func (x *LoginReply) Reset() { *x = LoginReply{} - if protoimpl.UnsafeEnabled { - mi := &file_core_v1_auth_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_v1_auth_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *LoginReply) String() string { @@ -148,7 +139,7 @@ func (*LoginReply) ProtoMessage() {} func (x *LoginReply) ProtoReflect() protoreflect.Message { mi := &file_core_v1_auth_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -178,20 +169,17 @@ func (x *LoginReply) GetToken() string { } type ActionReply struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + StatusCode int32 `protobuf:"varint,1,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` unknownFields protoimpl.UnknownFields - - StatusCode int32 `protobuf:"varint,1,opt,name=status_code,json=statusCode,proto3" json:"status_code,omitempty"` + sizeCache protoimpl.SizeCache } func (x *ActionReply) Reset() { *x = ActionReply{} - if protoimpl.UnsafeEnabled { - mi := &file_core_v1_auth_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_core_v1_auth_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *ActionReply) String() string { @@ -202,7 +190,7 @@ func (*ActionReply) ProtoMessage() {} func (x *ActionReply) ProtoReflect() protoreflect.Message { mi := &file_core_v1_auth_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -279,7 +267,7 @@ func file_core_v1_auth_proto_rawDescGZIP() []byte { } var file_core_v1_auth_proto_msgTypes = make([]protoimpl.MessageInfo, 4) -var file_core_v1_auth_proto_goTypes = []interface{}{ +var file_core_v1_auth_proto_goTypes = []any{ (*User)(nil), // 0: core.v1.User (*UserVerify)(nil), // 1: core.v1.UserVerify (*LoginReply)(nil), // 2: core.v1.LoginReply @@ -304,56 +292,6 @@ func file_core_v1_auth_proto_init() { if File_core_v1_auth_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_core_v1_auth_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*User); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_core_v1_auth_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*UserVerify); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_core_v1_auth_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*LoginReply); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_core_v1_auth_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*ActionReply); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/auto/rpc/core/v1/auth_grpc.pb.go b/auto/rpc/core/v1/auth_grpc.pb.go index 7cec3d856..ea3b07bfd 100644 --- a/auto/rpc/core/v1/auth_grpc.pb.go +++ b/auto/rpc/core/v1/auth_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.3.0 +// - protoc-gen-go-grpc v1.5.1 // - protoc (unknown) // source: core/v1/auth.proto @@ -15,8 +15,8 @@ import ( // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.32.0 or later. -const _ = grpc.SupportPackageIsVersion7 +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 const ( AuthenticateService_PreLogin_FullMethodName = "/core.v1.AuthenticateService/preLogin" @@ -42,8 +42,9 @@ func NewAuthenticateServiceClient(cc grpc.ClientConnInterface) AuthenticateServi } func (c *authenticateServiceClient) PreLogin(ctx context.Context, in *User, opts ...grpc.CallOption) (*ActionReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ActionReply) - err := c.cc.Invoke(ctx, AuthenticateService_PreLogin_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, AuthenticateService_PreLogin_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -51,8 +52,9 @@ func (c *authenticateServiceClient) PreLogin(ctx context.Context, in *User, opts } func (c *authenticateServiceClient) Login(ctx context.Context, in *User, opts ...grpc.CallOption) (*LoginReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(LoginReply) - err := c.cc.Invoke(ctx, AuthenticateService_Login_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, AuthenticateService_Login_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -60,8 +62,9 @@ func (c *authenticateServiceClient) Login(ctx context.Context, in *User, opts .. } func (c *authenticateServiceClient) Logout(ctx context.Context, in *User, opts ...grpc.CallOption) (*ActionReply, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(ActionReply) - err := c.cc.Invoke(ctx, AuthenticateService_Logout_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, AuthenticateService_Logout_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -70,7 +73,7 @@ func (c *authenticateServiceClient) Logout(ctx context.Context, in *User, opts . // AuthenticateServiceServer is the server API for AuthenticateService service. // All implementations must embed UnimplementedAuthenticateServiceServer -// for forward compatibility +// for forward compatibility. type AuthenticateServiceServer interface { PreLogin(context.Context, *User) (*ActionReply, error) Login(context.Context, *User) (*LoginReply, error) @@ -78,9 +81,12 @@ type AuthenticateServiceServer interface { mustEmbedUnimplementedAuthenticateServiceServer() } -// UnimplementedAuthenticateServiceServer must be embedded to have forward compatible implementations. -type UnimplementedAuthenticateServiceServer struct { -} +// UnimplementedAuthenticateServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedAuthenticateServiceServer struct{} func (UnimplementedAuthenticateServiceServer) PreLogin(context.Context, *User) (*ActionReply, error) { return nil, status.Errorf(codes.Unimplemented, "method PreLogin not implemented") @@ -92,6 +98,7 @@ func (UnimplementedAuthenticateServiceServer) Logout(context.Context, *User) (*A return nil, status.Errorf(codes.Unimplemented, "method Logout not implemented") } func (UnimplementedAuthenticateServiceServer) mustEmbedUnimplementedAuthenticateServiceServer() {} +func (UnimplementedAuthenticateServiceServer) testEmbeddedByValue() {} // UnsafeAuthenticateServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to AuthenticateServiceServer will @@ -101,6 +108,13 @@ type UnsafeAuthenticateServiceServer interface { } func RegisterAuthenticateServiceServer(s grpc.ServiceRegistrar, srv AuthenticateServiceServer) { + // If the following call pancis, it indicates UnimplementedAuthenticateServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } s.RegisterService(&AuthenticateService_ServiceDesc, srv) } diff --git a/auto/rpc/greet/v1/greet.pb.go b/auto/rpc/greet/v1/greet.pb.go index cd0e7fa08..296774e91 100644 --- a/auto/rpc/greet/v1/greet.pb.go +++ b/auto/rpc/greet/v1/greet.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.32.0 +// protoc-gen-go v1.36.3 // protoc (unknown) // source: greet/v1/greet.proto @@ -21,20 +21,17 @@ const ( ) type GreetRequest struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` unknownFields protoimpl.UnknownFields - - Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GreetRequest) Reset() { *x = GreetRequest{} - if protoimpl.UnsafeEnabled { - mi := &file_greet_v1_greet_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_greet_v1_greet_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GreetRequest) String() string { @@ -45,7 +42,7 @@ func (*GreetRequest) ProtoMessage() {} func (x *GreetRequest) ProtoReflect() protoreflect.Message { mi := &file_greet_v1_greet_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -68,20 +65,17 @@ func (x *GreetRequest) GetName() string { } type GreetResponse struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache + state protoimpl.MessageState `protogen:"open.v1"` + Greeting string `protobuf:"bytes,1,opt,name=greeting,proto3" json:"greeting,omitempty"` unknownFields protoimpl.UnknownFields - - Greeting string `protobuf:"bytes,1,opt,name=greeting,proto3" json:"greeting,omitempty"` + sizeCache protoimpl.SizeCache } func (x *GreetResponse) Reset() { *x = GreetResponse{} - if protoimpl.UnsafeEnabled { - mi := &file_greet_v1_greet_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } + mi := &file_greet_v1_greet_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) } func (x *GreetResponse) String() string { @@ -92,7 +86,7 @@ func (*GreetResponse) ProtoMessage() {} func (x *GreetResponse) ProtoReflect() protoreflect.Message { mi := &file_greet_v1_greet_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { + if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { ms.StoreMessageInfo(mi) @@ -154,7 +148,7 @@ func file_greet_v1_greet_proto_rawDescGZIP() []byte { } var file_greet_v1_greet_proto_msgTypes = make([]protoimpl.MessageInfo, 2) -var file_greet_v1_greet_proto_goTypes = []interface{}{ +var file_greet_v1_greet_proto_goTypes = []any{ (*GreetRequest)(nil), // 0: greet.v1.GreetRequest (*GreetResponse)(nil), // 1: greet.v1.GreetResponse } @@ -173,32 +167,6 @@ func file_greet_v1_greet_proto_init() { if File_greet_v1_greet_proto != nil { return } - if !protoimpl.UnsafeEnabled { - file_greet_v1_greet_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GreetRequest); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_greet_v1_greet_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*GreetResponse); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/auto/rpc/greet/v1/greet_grpc.pb.go b/auto/rpc/greet/v1/greet_grpc.pb.go index f1667a546..be277a6bb 100644 --- a/auto/rpc/greet/v1/greet_grpc.pb.go +++ b/auto/rpc/greet/v1/greet_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.3.0 +// - protoc-gen-go-grpc v1.5.1 // - protoc (unknown) // source: greet/v1/greet.proto @@ -15,8 +15,8 @@ import ( // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -// Requires gRPC-Go v1.32.0 or later. -const _ = grpc.SupportPackageIsVersion7 +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 const ( GreetService_Greet_FullMethodName = "/greet.v1.GreetService/Greet" @@ -38,8 +38,9 @@ func NewGreetServiceClient(cc grpc.ClientConnInterface) GreetServiceClient { } func (c *greetServiceClient) Greet(ctx context.Context, in *GreetRequest, opts ...grpc.CallOption) (*GreetResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(GreetResponse) - err := c.cc.Invoke(ctx, GreetService_Greet_FullMethodName, in, out, opts...) + err := c.cc.Invoke(ctx, GreetService_Greet_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } @@ -48,20 +49,24 @@ func (c *greetServiceClient) Greet(ctx context.Context, in *GreetRequest, opts . // GreetServiceServer is the server API for GreetService service. // All implementations must embed UnimplementedGreetServiceServer -// for forward compatibility +// for forward compatibility. type GreetServiceServer interface { Greet(context.Context, *GreetRequest) (*GreetResponse, error) mustEmbedUnimplementedGreetServiceServer() } -// UnimplementedGreetServiceServer must be embedded to have forward compatible implementations. -type UnimplementedGreetServiceServer struct { -} +// UnimplementedGreetServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedGreetServiceServer struct{} func (UnimplementedGreetServiceServer) Greet(context.Context, *GreetRequest) (*GreetResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method Greet not implemented") } func (UnimplementedGreetServiceServer) mustEmbedUnimplementedGreetServiceServer() {} +func (UnimplementedGreetServiceServer) testEmbeddedByValue() {} // UnsafeGreetServiceServer may be embedded to opt out of forward compatibility for this service. // Use of this interface is not recommended, as added methods to GreetServiceServer will @@ -71,6 +76,13 @@ type UnsafeGreetServiceServer interface { } func RegisterGreetServiceServer(s grpc.ServiceRegistrar, srv GreetServiceServer) { + // If the following call pancis, it indicates UnimplementedGreetServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } s.RegisterService(&GreetService_ServiceDesc, srv) } diff --git a/go.mod b/go.mod index 6f2889733..ed67ba5d8 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/Masterminds/semver/v3 v3.2.1 github.com/RoaringBitmap/roaring v1.9.4 github.com/afocus/captcha v0.0.0-20191010092841-4bd1f21c8868 - github.com/alimy/mir/v4 v4.2.0-alpha.5 + github.com/alimy/mir/v4 v4.2.0-alpha.6 github.com/alimy/tryst v0.22.0 github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible github.com/allegro/bigcache/v3 v3.1.0 diff --git a/go.sum b/go.sum index 3fdcccf56..8c0df2387 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,8 @@ github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90= github.com/afocus/captcha v0.0.0-20191010092841-4bd1f21c8868 h1:uFrPOl1VBt/Abfl2z+A/DFc+AwmFLxEHR1+Yq6cXvww= github.com/afocus/captcha v0.0.0-20191010092841-4bd1f21c8868/go.mod h1:srphKZ1i+yGXxl/LpBS7ZIECTjCTPzZzAMtJWoG3sLo= -github.com/alimy/mir/v4 v4.2.0-alpha.5 h1:ExSJpbFzKX3Avk1CoTOU3OLyvo4PTB2SnTSQXfeJNIc= -github.com/alimy/mir/v4 v4.2.0-alpha.5/go.mod h1:d58dBvw2KImcVbAUANrciEV/of0arMNsI9c/5UNCMMc= +github.com/alimy/mir/v4 v4.2.0-alpha.6 h1:ADdpC7zI2Chs4oddvTVu76uif2I9TLbNCpCwS+8MtWk= +github.com/alimy/mir/v4 v4.2.0-alpha.6/go.mod h1:d58dBvw2KImcVbAUANrciEV/of0arMNsI9c/5UNCMMc= github.com/alimy/tryst v0.22.0 h1:tjZFvHliMDkymEZuuhH/e6Tg41B72LJt8/5TCiJztGw= github.com/alimy/tryst v0.22.0/go.mod h1:HPOlTam3dT+of3slvIxpzf1pUQEUAfBJp1zgIuk/uLY= github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible h1:8psS8a+wKfiLt1iVDX79F7Y6wUM49Lcha2FMXt4UM8g= diff --git a/internal/model/joint/page.go b/internal/model/joint/page.go index f2ba5d9c1..364639b8a 100644 --- a/internal/model/joint/page.go +++ b/internal/model/joint/page.go @@ -4,6 +4,8 @@ package joint +import "github.com/rocboss/paopao-ce/internal/conf" + type Pager struct { Page int `json:"page"` PageSize int `json:"page_size"` @@ -15,6 +17,11 @@ type PageResp struct { Pager Pager `json:"pager"` } +type PageInfo struct { + Limit int `json:"-" form:"-" query:"limit" binding:"-"` + Offset int `json:"-" form:"-" query:"offset" binding:"-"` +} + func PageRespFrom(list any, page int, pageSize int, totalRows int64) *PageResp { return &PageResp{ List: list, @@ -25,3 +32,15 @@ func PageRespFrom(list any, page int, pageSize int, totalRows int64) *PageResp { }, } } + +func (s *PageInfo) BuildPageInfo(page, size int) { + if page <= 0 { + page = 1 + } + if size <= 0 { + s.Limit = conf.AppSetting.DefaultPageSize + } else if size > conf.AppSetting.MaxPageSize { + s.Limit = conf.AppSetting.MaxPageSize + } + s.Offset = (page - 1) * s.Limit +} diff --git a/internal/model/v2/README.md b/internal/model/v2/README.md new file mode 100644 index 000000000..34f8aa2c6 --- /dev/null +++ b/internal/model/v2/README.md @@ -0,0 +1 @@ +web v2 model diff --git a/internal/model/v2/web/loose.go b/internal/model/v2/web/loose.go new file mode 100644 index 000000000..cf3d5562d --- /dev/null +++ b/internal/model/v2/web/loose.go @@ -0,0 +1,159 @@ +// Copyright 2025 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package web + +import ( + "github.com/rocboss/paopao-ce/internal/conf" + "github.com/rocboss/paopao-ce/internal/core" + "github.com/rocboss/paopao-ce/internal/core/cs" + "github.com/rocboss/paopao-ce/internal/core/ms" + "github.com/rocboss/paopao-ce/internal/model/joint" +) + +const ( + TagTypeHot = cs.TagTypeHot + TagTypeNew = cs.TagTypeNew + TagTypeFollow = cs.TagTypeFollow + TagTypePin = cs.TagTypePin + TagTypeHotExtral = cs.TagTypeHotExtral +) + +const ( + UserPostsStylePost = "post" + UserPostsStyleComment = "comment" + UserPostsStyleHighlight = "highlight" + UserPostsStyleMedia = "media" + UserPostsStyleStar = "star" + + StyleTweetsNewest = "newest" + StyleTweetsHots = "hots" + StyleTweetsFollowing = "following" +) + +type TagType = cs.TagType + +type CommentStyleType string + +type TweetCommentsReq struct { + SimpleInfo + joint.PageInfo + TweetId int64 `form:"id" query:"id" binding:"required"` + Style CommentStyleType `form:"style" query:"style"` + Page int `form:"page" query:"page" binding:"-"` + PageSize int `form:"page_size" query:"page_size" binding:"-"` +} + +type TweetCommentsResp struct { + joint.CachePageResp +} + +type TimelineReq struct { + BaseInfo + joint.PageInfo + Query string `form:"query" query:"query"` + Visibility []core.PostVisibleT `form:"visibility" query:"visibility"` + Type string `form:"type" query:"type"` + Style string `form:"style" query:"query"` + Page int `form:"page" query:"page"` + PageSize int `form:"page_size" query:"page_size"` +} + +type TimelineResp struct { + joint.CachePageResp +} + +type GetUserTweetsReq struct { + BaseInfo `form:"-" binding:"-"` + joint.PageInfo + Username string `form:"username" query:"username" binding:"required"` + Style string `form:"style" query:"style"` + Page int `form:"page" query:"page"` + PageSize int `form:"page_size" query:"page_size"` +} + +type GetUserTweetsResp struct { + joint.CachePageResp +} + +type GetUserProfileReq struct { + BaseInfo + Username string `form:"username" query:"username" binding:"required"` +} + +type GetUserProfileResp struct { + ID int64 `json:"id"` + Nickname string `json:"nickname"` + Username string `json:"username"` + Status int `json:"status"` + Avatar string `json:"avatar"` + IsAdmin bool `json:"is_admin"` + IsFriend bool `json:"is_friend"` + IsFollowing bool `json:"is_following"` + CreatedOn int64 `json:"created_on"` + Follows int64 `json:"follows"` + Followings int64 `json:"followings"` + TweetsCount int `json:"tweets_count"` +} + +type TopicListReq struct { + SimpleInfo + Type TagType `json:"type" form:"type" query:"type" binding:"required"` + Num int `json:"num" form:"num" query:"num" binding:"required"` + ExtralNum int `json:"extral_num" form:"extral_num" query:"extral_num"` +} + +// TopicListResp 主题返回值 +// TODO: 优化内容定义 +type TopicListResp struct { + Topics cs.TagList `json:"topics"` + ExtralTopics cs.TagList `json:"extral_topics,omitempty"` +} + +type TweetDetailReq struct { + BaseInfo + TweetId int64 `form:"id" query:"id"` +} + +type TweetDetailResp ms.PostFormated + +func (r *GetUserTweetsReq) Ajust(page int, pageSize int) { + r.BuildPageInfo(r.Page, r.PageSize) +} + +func (r *TweetCommentsReq) Ajust(page int, pageSize int) { + r.BuildPageInfo(r.Page, r.PageSize) +} + +func (r *TimelineReq) Ajust() { + r.BuildPageInfo(r.Page, r.PageSize) +} + +func (s CommentStyleType) ToInnerValue() (res cs.StyleCommentType) { + switch s { + case "hots": + res = cs.StyleCommentHots + case "newest": + res = cs.StyleCommentNewest + case "default": + fallthrough + default: + res = cs.StyleCommentDefault + } + return +} + +func (s CommentStyleType) String() (res string) { + switch s { + case "default": + res = conf.InfixCommentDefault + case "hots": + res = conf.InfixCommentHots + case "newest": + res = conf.InfixCommentNewest + default: + res = "_" + } + return +} diff --git a/internal/model/v2/web/web.go b/internal/model/v2/web/web.go new file mode 100644 index 000000000..90a986a31 --- /dev/null +++ b/internal/model/v2/web/web.go @@ -0,0 +1,63 @@ +// Copyright 2025 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package web + +import ( + "github.com/alimy/mir/v4" + "github.com/gin-gonic/gin" + "github.com/rocboss/paopao-ce/internal/core/ms" + "github.com/rocboss/paopao-ce/internal/servants/base" + "github.com/rocboss/paopao-ce/pkg/app" + "github.com/rocboss/paopao-ce/pkg/xerror" +) + +var ( + bindAny = base.NewBindAnyFn() +) + +type BaseInfo struct { + User *ms.User `json:"-" form:"-" query:"limit" binding:"-"` +} + +type SimpleInfo struct { + Uid int64 `json:"-" form:"-" query:"limit" binding:"-"` +} + +type BasePageReq struct { + UserId int64 + Page int + PageSize int +} + +func (b *BaseInfo) SetUser(user *ms.User) { + b.User = user +} + +func (s *SimpleInfo) SetUserId(id int64) { + s.Uid = id +} + +func BasePageReqFrom(c *gin.Context) (*BasePageReq, mir.Error) { + uid, ok := base.UserIdFrom(c) + if !ok { + return nil, xerror.UnauthorizedTokenError + } + page, pageSize := app.GetPageInfo(c) + return &BasePageReq{ + UserId: uid, + Page: page, + PageSize: pageSize, + }, nil +} + +func (r *BasePageReq) Bind(c *gin.Context) mir.Error { + uid, ok := base.UserIdFrom(c) + if !ok { + return xerror.UnauthorizedTokenError + } + r.UserId = uid + r.Page, r.PageSize = app.GetPageInfo(c) + return nil +} diff --git a/internal/model/v2/web/xerror.go b/internal/model/v2/web/xerror.go new file mode 100644 index 000000000..e98c4b2c1 --- /dev/null +++ b/internal/model/v2/web/xerror.go @@ -0,0 +1,109 @@ +// Copyright 2025 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package web + +import ( + "github.com/rocboss/paopao-ce/pkg/xerror" +) + +// nolint +var ( + ErrUsernameHasExisted = xerror.NewError(20001, "用户名已存在") + ErrUsernameLengthLimit = xerror.NewError(20002, "用户名长度3~12") + ErrUsernameCharLimit = xerror.NewError(20003, "用户名只能包含字母、数字") + ErrPasswordLengthLimit = xerror.NewError(20004, "密码长度6~16") + ErrUserRegisterFailed = xerror.NewError(20005, "用户注册失败") + ErrUserHasBeenBanned = xerror.NewError(20006, "该账户已被封停") + ErrNoPermission = xerror.NewError(20007, "无权限执行该请求") + ErrUserHasBindOTP = xerror.NewError(20008, "当前用户已绑定二次验证") + ErrUserOTPInvalid = xerror.NewError(20009, "二次验证码验证失败") + ErrUserNoBindOTP = xerror.NewError(20010, "当前用户未绑定二次验证") + ErrErrorOldPassword = xerror.NewError(20011, "当前用户密码验证失败") + ErrErrorCaptchaPassword = xerror.NewError(20012, "图形验证码验证失败") + ErrAccountNoPhoneBind = xerror.NewError(20013, "拒绝操作: 账户未绑定手机号") + ErrTooManyLoginError = xerror.NewError(20014, "登录失败次数过多,请稍后再试") + ErrGetPhoneCaptchaError = xerror.NewError(20015, "短信验证码获取失败") + ErrTooManyPhoneCaptchaSend = xerror.NewError(20016, "短信验证码获取次数已达今日上限") + ErrExistedUserPhone = xerror.NewError(20017, "该手机号已被绑定") + ErrErrorPhoneCaptcha = xerror.NewError(20018, "手机验证码不正确") + ErrMaxPhoneCaptchaUseTimes = xerror.NewError(20019, "手机验证码已达最大使用次数") + ErrNicknameLengthLimit = xerror.NewError(20020, "昵称长度2~12") + ErrNoExistUsername = xerror.NewError(20021, "用户不存在") + ErrNoAdminPermission = xerror.NewError(20022, "无管理权限") + ErrDisallowUserRegister = xerror.NewError(20023, "系统不允许注册用户") + + ErrGetPostsFailed = xerror.NewError(30001, "获取动态列表失败") + ErrCreatePostFailed = xerror.NewError(30002, "动态发布失败") + ErrGetPostFailed = xerror.NewError(30003, "获取动态详情失败") + ErrDeletePostFailed = xerror.NewError(30004, "动态删除失败") + ErrLockPostFailed = xerror.NewError(30005, "动态锁定失败") + ErrGetPostTagsFailed = xerror.NewError(30006, "获取话题列表失败") + ErrInvalidDownloadReq = xerror.NewError(30007, "附件下载请求不合法") + ErrDownloadReqError = xerror.NewError(30008, "附件下载请求失败") + ErrInsuffientDownloadMoney = xerror.NewError(30009, "附件下载失败:账户资金不足") + ErrDownloadExecFail = xerror.NewError(30010, "附件下载失败:扣费失败") + ErrStickPostFailed = xerror.NewError(30011, "动态置顶失败") + ErrVisblePostFailed = xerror.NewError(30012, "更新可见性失败") + ErrHighlightPostFailed = xerror.NewError(30013, "动态设为亮点失败") + ErrGetPostsUnknowStyle = xerror.NewError(30014, "使用未知样式参数获取动态列表") + ErrGetPostsNilUser = xerror.NewError(30015, "使用游客账户获取动态详情失败") + + ErrGetCommentsFailed = xerror.NewError(40001, "获取评论列表失败") + ErrCreateCommentFailed = xerror.NewError(40002, "评论发布失败") + ErrGetCommentFailed = xerror.NewError(40003, "获取评论详情失败") + ErrDeleteCommentFailed = xerror.NewError(40004, "评论删除失败") + ErrCreateReplyFailed = xerror.NewError(40005, "评论回复失败") + ErrGetReplyFailed = xerror.NewError(40006, "获取评论详情失败") + ErrMaxCommentCount = xerror.NewError(40007, "评论数已达最大限制") + ErrGetCommentThumbs = xerror.NewError(40008, "获取评论点赞信息失败") + ErrHighlightCommentFailed = xerror.NewError(40009, "设置精选评论失败") + + ErrGetMessagesFailed = xerror.NewError(50001, "获取消息列表失败") + ErrReadMessageFailed = xerror.NewError(50002, "标记消息已读失败") + ErrSendWhisperFailed = xerror.NewError(50003, "私信发送失败") + ErrNoWhisperToSelf = xerror.NewError(50004, "不允许给自己发送私信") + ErrTooManyWhisperNum = xerror.NewError(50005, "今日私信次数已达上限") + + ErrGetCollectionsFailed = xerror.NewError(60001, "获取收藏列表失败") + ErrGetStarsFailed = xerror.NewError(60002, "获取点赞列表失败") + + ErrRechargeReqFail = xerror.NewError(70001, "充值请求失败") + ErrRechargeNotifyError = xerror.NewError(70002, "充值回调失败") + ErrGetRechargeFailed = xerror.NewError(70003, "充值详情获取失败") + ErrUserWalletBillsFailed = xerror.NewError(70004, "用户钱包账单获取失败") + + ErrNoRequestingFriendToSelf = xerror.NewError(80001, "不允许添加自己为好友") + ErrNotExistFriendId = xerror.NewError(80002, "好友id不存在") + ErrSendRequestingFriendFailed = xerror.NewError(80003, "申请添加朋友请求发送失败") + ErrAddFriendFailed = xerror.NewError(80004, "添加好友失败") + ErrRejectFriendFailed = xerror.NewError(80005, "拒绝好友失败") + ErrDeleteFriendFailed = xerror.NewError(80006, "删除好友失败") + ErrGetContactsFailed = xerror.NewError(80007, "获取联系人列表失败") + ErrNoActionToSelf = xerror.NewError(80008, "不允许对自己操作") + ErrFolloUserFailed = xerror.NewError(80100, "关注失败") + ErrUnfollowUserFailed = xerror.NewError(80101, "取消关注失败") + ErrListFollowsFailed = xerror.NewError(80102, "获取关注列表失败") + ErrListFollowingsFailed = xerror.NewError(80103, "获取粉丝列表列表失败") + ErrGetFollowCountFailed = xerror.NewError(80104, "获取关注计数信息失败") + ErrNotAllowFollowSelf = xerror.NewError(80105, "不能关注自己") + ErrNotAllowUnfollowSelf = xerror.NewError(80106, "不能取消关注自己") + + ErrGetIndexTrendsFailed = xerror.NewError(802001, "获取动态条栏信息失败") + + ErrFollowTopicFailed = xerror.NewError(90001, "关注话题失败") + ErrUnfollowTopicFailed = xerror.NewError(90002, "取消关注话题失败") + ErrStickTopicFailed = xerror.NewError(90003, "更行话题置顶状态失败") + ErrPinTopicFailed = xerror.NewError(90005, "更行话题钉住状态失败") + ErrThumbsUpTweetComment = xerror.NewError(90101, "评论点赞失败") + ErrThumbsDownTweetComment = xerror.NewError(90102, "评论点踩失败") + ErrThumbsUpTweetReply = xerror.NewError(90103, "评论回复点赞失败") + ErrThumbsDownTweetReply = xerror.NewError(90104, "评论回复点踩失败") + + ErrFileUploadFailed = xerror.NewError(10200, "文件上传失败") + ErrFileInvalidExt = xerror.NewError(10201, "文件类型不合法") + ErrFileInvalidSize = xerror.NewError(10202, "文件大小超限") + + ErrNotImplemented = xerror.NewError(10501, "功能未实现") +) diff --git a/internal/model/web/loose.go b/internal/model/web/loose.go index 8ca847f09..04f95cc70 100644 --- a/internal/model/web/loose.go +++ b/internal/model/web/loose.go @@ -55,7 +55,7 @@ type TweetCommentsResp struct { type TimelineReq struct { BaseInfo `form:"-" binding:"-"` Query string `form:"query"` - Visibility []core.PostVisibleT `form:"query"` + Visibility []core.PostVisibleT `form:"visibility"` Type string `form:"type"` Style string `form:"style"` Page int `form:"-" binding:"-"` diff --git a/internal/servants/base/base.go b/internal/servants/base/base.go index 3f4d9d108..cc29d1864 100644 --- a/internal/servants/base/base.go +++ b/internal/servants/base/base.go @@ -15,6 +15,7 @@ import ( "github.com/getsentry/sentry-go" sentrygin "github.com/getsentry/sentry-go/gin" "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" "github.com/rocboss/paopao-ce/internal/conf" "github.com/rocboss/paopao-ce/internal/core" "github.com/rocboss/paopao-ce/internal/core/cs" @@ -29,8 +30,11 @@ import ( ) type BaseServant struct { - bindAny func(c *gin.Context, obj any) mir.Error - bindJson func(c *gin.Context, obj any) mir.Error + bindAny func(c *gin.Context, obj any) mir.Error + bindJson func(c *gin.Context, obj any) mir.Error + bindQeury func(c *gin.Context, obj any) mir.Error + bindForm func(c *gin.Context, obj any) mir.Error + bindMultipart func(c *gin.Context, obj any) mir.Error } type DaoServant struct { @@ -58,6 +62,10 @@ type PageInfoSetter interface { SetPageInfo(page, pageSize int) } +type Ajustment interface { + Ajust() +} + func UserFrom(c *gin.Context) (*ms.User, bool) { if u, exists := c.Get("USER"); exists { user, ok := u.(*ms.User) @@ -82,6 +90,90 @@ func UserNameFrom(c *gin.Context) (string, bool) { return "", false } +func bindQeury(c *gin.Context, obj any) mir.Error { + var errs xerror.ValidErrors + err := c.BindQuery(obj) + if err != nil { + return mir.NewError(xerror.InvalidParams.StatusCode(), xerror.InvalidParams.WithDetails(errs.Error())) + } + // setup *core.User if needed + if setter, ok := obj.(UserSetter); ok { + user, _ := UserFrom(c) + setter.SetUser(user) + } + // setup UserId if needed + if setter, ok := obj.(UserIdSetter); ok { + uid, _ := UserIdFrom(c) + setter.SetUserId(uid) + } + // setup PageInfo if needed + if setter, ok := obj.(PageInfoSetter); ok { + page, pageSize := app.GetPageInfo(c) + setter.SetPageInfo(page, pageSize) + } + // ajust object fields value + if ajustment, ok := obj.(Ajustment); ok { + ajustment.Ajust() + } + return nil +} + +func bindMultipart(c *gin.Context, obj any) mir.Error { + var errs xerror.ValidErrors + err := c.MustBindWith(obj, binding.FormMultipart) + if err != nil { + return mir.NewError(xerror.InvalidParams.StatusCode(), xerror.InvalidParams.WithDetails(errs.Error())) + } + // setup *core.User if needed + if setter, ok := obj.(UserSetter); ok { + user, _ := UserFrom(c) + setter.SetUser(user) + } + // setup UserId if needed + if setter, ok := obj.(UserIdSetter); ok { + uid, _ := UserIdFrom(c) + setter.SetUserId(uid) + } + // setup PageInfo if needed + if setter, ok := obj.(PageInfoSetter); ok { + page, pageSize := app.GetPageInfo(c) + setter.SetPageInfo(page, pageSize) + } + // ajust object fields value + if ajustment, ok := obj.(Ajustment); ok { + ajustment.Ajust() + } + return nil +} + +func bindForm(c *gin.Context, obj any) mir.Error { + var errs xerror.ValidErrors + err := c.MustBindWith(obj, binding.Form) + if err != nil { + return mir.NewError(xerror.InvalidParams.StatusCode(), xerror.InvalidParams.WithDetails(errs.Error())) + } + // setup *core.User if needed + if setter, ok := obj.(UserSetter); ok { + user, _ := UserFrom(c) + setter.SetUser(user) + } + // setup UserId if needed + if setter, ok := obj.(UserIdSetter); ok { + uid, _ := UserIdFrom(c) + setter.SetUserId(uid) + } + // setup PageInfo if needed + if setter, ok := obj.(PageInfoSetter); ok { + page, pageSize := app.GetPageInfo(c) + setter.SetPageInfo(page, pageSize) + } + // ajust object fields value + if ajustment, ok := obj.(Ajustment); ok { + ajustment.Ajust() + } + return nil +} + func bindAny(c *gin.Context, obj any) mir.Error { var errs xerror.ValidErrors err := c.ShouldBind(obj) @@ -103,6 +195,10 @@ func bindAny(c *gin.Context, obj any) mir.Error { page, pageSize := app.GetPageInfo(c) setter.SetPageInfo(page, pageSize) } + // ajust object fields value + if ajustment, ok := obj.(Ajustment); ok { + ajustment.Ajust() + } return nil } @@ -136,8 +232,122 @@ func bindAnySentry(c *gin.Context, obj any) mir.Error { page, pageSize := app.GetPageInfo(c) setter.SetPageInfo(page, pageSize) } + // ajust object fields value + if ajustment, ok := obj.(Ajustment); ok { + ajustment.Ajust() + } + return nil +} + +func bindQuerySentry(c *gin.Context, obj any) mir.Error { + hub := sentrygin.GetHubFromContext(c) + var errs xerror.ValidErrors + err := c.BindQuery(obj) + if err != nil { + xerr := mir.NewError(xerror.InvalidParams.StatusCode(), xerror.InvalidParams.WithDetails(errs.Error())) + if hub != nil { + hub.CaptureException(errors.Wrap(xerr, "bind object")) + } + return xerr + } + // setup sentry hub if needed + if setter, ok := obj.(SentryHubSetter); ok && hub != nil { + setter.SetSentryHub(hub) + } + // setup *core.User if needed + if setter, ok := obj.(UserSetter); ok { + user, _ := UserFrom(c) + setter.SetUser(user) + } + // setup UserId if needed + if setter, ok := obj.(UserIdSetter); ok { + uid, _ := UserIdFrom(c) + setter.SetUserId(uid) + } + // setup PageInfo if needed + if setter, ok := obj.(PageInfoSetter); ok { + page, pageSize := app.GetPageInfo(c) + setter.SetPageInfo(page, pageSize) + } + // ajust object fields value + if ajustment, ok := obj.(Ajustment); ok { + ajustment.Ajust() + } return nil +} +func bindFormSentry(c *gin.Context, obj any) mir.Error { + hub := sentrygin.GetHubFromContext(c) + var errs xerror.ValidErrors + err := c.MustBindWith(obj, binding.Form) + if err != nil { + xerr := mir.NewError(xerror.InvalidParams.StatusCode(), xerror.InvalidParams.WithDetails(errs.Error())) + if hub != nil { + hub.CaptureException(errors.Wrap(xerr, "bind object")) + } + return xerr + } + // setup sentry hub if needed + if setter, ok := obj.(SentryHubSetter); ok && hub != nil { + setter.SetSentryHub(hub) + } + // setup *core.User if needed + if setter, ok := obj.(UserSetter); ok { + user, _ := UserFrom(c) + setter.SetUser(user) + } + // setup UserId if needed + if setter, ok := obj.(UserIdSetter); ok { + uid, _ := UserIdFrom(c) + setter.SetUserId(uid) + } + // setup PageInfo if needed + if setter, ok := obj.(PageInfoSetter); ok { + page, pageSize := app.GetPageInfo(c) + setter.SetPageInfo(page, pageSize) + } + // ajust object fields value + if ajustment, ok := obj.(Ajustment); ok { + ajustment.Ajust() + } + return nil +} + +func bindMultipartSentry(c *gin.Context, obj any) mir.Error { + hub := sentrygin.GetHubFromContext(c) + var errs xerror.ValidErrors + err := c.MustBindWith(obj, binding.FormMultipart) + if err != nil { + xerr := mir.NewError(xerror.InvalidParams.StatusCode(), xerror.InvalidParams.WithDetails(errs.Error())) + if hub != nil { + hub.CaptureException(errors.Wrap(xerr, "bind object")) + } + return xerr + } + // setup sentry hub if needed + if setter, ok := obj.(SentryHubSetter); ok && hub != nil { + setter.SetSentryHub(hub) + } + // setup *core.User if needed + if setter, ok := obj.(UserSetter); ok { + user, _ := UserFrom(c) + setter.SetUser(user) + } + // setup UserId if needed + if setter, ok := obj.(UserIdSetter); ok { + uid, _ := UserIdFrom(c) + setter.SetUserId(uid) + } + // setup PageInfo if needed + if setter, ok := obj.(PageInfoSetter); ok { + page, pageSize := app.GetPageInfo(c) + setter.SetPageInfo(page, pageSize) + } + // ajust object fields value + if ajustment, ok := obj.(Ajustment); ok { + ajustment.Ajust() + } + return nil } func RenderAny(c *gin.Context, data any, err mir.Error) { @@ -163,6 +373,18 @@ func (s *BaseServant) BindJson(c *gin.Context, obj any) mir.Error { return s.bindJson(c, obj) } +func (s *BaseServant) BindQuery(c *gin.Context, obj any) mir.Error { + return s.bindQeury(c, obj) +} + +func (s *BaseServant) BindForm(c *gin.Context, obj any) mir.Error { + return s.bindForm(c, obj) +} + +func (s *BaseServant) BindMultipart(c *gin.Context, obj any) mir.Error { + return s.bindMultipart(c, obj) +} + func (s *BaseServant) Render(c *gin.Context, data any, err mir.Error) { if err == nil { c.JSON(http.StatusOK, &joint.JsonResp{ @@ -424,6 +646,27 @@ func NewBindAnyFn() func(c *gin.Context, obj any) mir.Error { return bindAny } +func NewBindQueryFn() func(c *gin.Context, obj any) mir.Error { + if conf.UseSentryGin() { + return bindQuerySentry + } + return bindQeury +} + +func NewBindFormFn() func(c *gin.Context, obj any) mir.Error { + if conf.UseSentryGin() { + return bindFormSentry + } + return bindForm +} + +func NewBindMultipart() func(c *gin.Context, obj any) mir.Error { + if conf.UseSentryGin() { + return bindMultipartSentry + } + return bindMultipart +} + func NewBindJsonFn() func(c *gin.Context, obj any) mir.Error { if conf.UseSentryGin() { return bindAnySentry @@ -433,8 +676,11 @@ func NewBindJsonFn() func(c *gin.Context, obj any) mir.Error { func NewBaseServant() *BaseServant { return &BaseServant{ - bindAny: NewBindAnyFn(), - bindJson: NewBindJsonFn(), + bindAny: NewBindAnyFn(), + bindJson: NewBindJsonFn(), + bindQeury: NewBindQueryFn(), + bindForm: NewBindFormFn(), + bindMultipart: NewBindMultipart(), } } diff --git a/internal/servants/servants.go b/internal/servants/servants.go index 5d56b7e5b..5fab47662 100644 --- a/internal/servants/servants.go +++ b/internal/servants/servants.go @@ -19,6 +19,7 @@ import ( "github.com/rocboss/paopao-ce/internal/servants/statick" "github.com/rocboss/paopao-ce/internal/servants/triplet" "github.com/rocboss/paopao-ce/internal/servants/web" + webv2 "github.com/rocboss/paopao-ce/internal/servants/web/v2" "google.golang.org/grpc" ) @@ -31,6 +32,8 @@ func RegisterWebServants(e *gin.Engine) { localoss.RouteLocalOSS(e) }) web.RouteWeb(e) + // web v2 api + webv2.RouteWeb(e) } // RegisterAdminServants register all the servants to gin.Engine diff --git a/internal/servants/web/v2/loose.go b/internal/servants/web/v2/loose.go new file mode 100644 index 000000000..202235f8b --- /dev/null +++ b/internal/servants/web/v2/loose.go @@ -0,0 +1,562 @@ +// Copyright 2025 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package web + +import ( + "fmt" + + "github.com/alimy/mir/v4" + "github.com/gin-gonic/gin" + api "github.com/rocboss/paopao-ce/auto/api/v2" + "github.com/rocboss/paopao-ce/internal/conf" + "github.com/rocboss/paopao-ce/internal/core" + "github.com/rocboss/paopao-ce/internal/core/cs" + "github.com/rocboss/paopao-ce/internal/core/ms" + "github.com/rocboss/paopao-ce/internal/dao/jinzhu/dbr" + "github.com/rocboss/paopao-ce/internal/model/joint" + "github.com/rocboss/paopao-ce/internal/model/v2/web" + "github.com/rocboss/paopao-ce/internal/servants/base" + "github.com/rocboss/paopao-ce/internal/servants/chain" + "github.com/sirupsen/logrus" +) + +var ( + _ api.Loose = (*looseSrv)(nil) +) + +type looseSrv struct { + api.UnimplementedLooseServant + *base.DaoServant + ac core.AppCache + userTweetsExpire int64 + idxTweetsExpire int64 + tweetCommentsExpire int64 + prefixUserTweets string + prefixIdxTweetsNewest string + prefixIdxTweetsHots string + prefixIdxTweetsFollowing string + prefixTweetComment string +} + +func (s *looseSrv) Chain() gin.HandlersChain { + return gin.HandlersChain{chain.JwtLoose()} +} + +func (s *looseSrv) Timeline(req *web.TimelineReq) (*web.TimelineResp, mir.Error) { + if req.Query == "" && req.Type == "search" { + return s.getIndexTweets(req, req.Limit, req.Offset) + } + q := &core.QueryReq{ + Query: req.Query, + Type: core.SearchType(req.Type), + } + res, err := s.Ts.Search(req.User, q, req.Offset, req.Limit) + if err != nil { + logrus.Errorf("Ts.Search err: %s", err) + return nil, web.ErrGetPostsFailed + } + posts, err := s.Ds.RevampPosts(res.Items) + if err != nil { + logrus.Errorf("Ds.RevampPosts err: %s", err) + return nil, web.ErrGetPostsFailed + } + userId := int64(-1) + if req.User != nil { + userId = req.User.ID + } + if err := s.PrepareTweets(userId, posts); err != nil { + logrus.Errorf("timeline occurs error[2]: %s", err) + return nil, web.ErrGetPostsFailed + } + resp := joint.PageRespFrom(posts, req.Page, req.PageSize, res.Total) + return &web.TimelineResp{ + CachePageResp: joint.CachePageResp{ + Data: resp, + }, + }, nil +} + +func (s *looseSrv) getIndexTweets(req *web.TimelineReq, limit int, offset int) (res *web.TimelineResp, err mir.Error) { + // 尝试直接从缓存中获取数据 + key, ok := "", false + if res, key, ok = s.indexTweetsFromCache(req, limit, offset); ok { + // logrus.Debugf("getIndexTweets from cache key:%s", key) + return + } + var ( + posts []*ms.Post + total int64 + xerr error + ) + switch req.Style { + case web.StyleTweetsFollowing: + if req.User != nil { + posts, total, xerr = s.Ds.ListFollowingTweets(req.User.ID, limit, offset) + } else { + // return nil, web.ErrGetPostsNilUser + // 宽松处理,前端退出登录后马上获取动态列表,可能错误走到这里 + posts, total, xerr = s.Ds.ListIndexNewestTweets(limit, offset) + } + case web.StyleTweetsNewest: + posts, total, xerr = s.Ds.ListIndexNewestTweets(limit, offset) + case web.StyleTweetsHots: + posts, total, xerr = s.Ds.ListIndexHotsTweets(limit, offset) + default: + return nil, web.ErrGetPostsUnknowStyle + } + if xerr != nil { + logrus.Errorf("getIndexTweets occurs error[1]: %s", xerr) + return nil, web.ErrGetPostFailed + } + postsFormated, verr := s.Ds.MergePosts(posts) + if verr != nil { + logrus.Errorf("getIndexTweets in merge posts occurs error: %s", verr) + return nil, web.ErrGetPostFailed + } + userId := int64(-1) + if req.User != nil { + userId = req.User.ID + } + if err := s.PrepareTweets(userId, postsFormated); err != nil { + logrus.Errorf("getIndexTweets occurs error[2]: %s", err) + return nil, web.ErrGetPostsFailed + } + resp := joint.PageRespFrom(postsFormated, req.Page, req.PageSize, total) + // 缓存处理 + base.OnCacheRespEvent(s.ac, key, resp, s.idxTweetsExpire) + return &web.TimelineResp{ + CachePageResp: joint.CachePageResp{ + Data: resp, + }, + }, nil +} + +func (s *looseSrv) indexTweetsFromCache(req *web.TimelineReq, limit int, offset int) (res *web.TimelineResp, key string, ok bool) { + username := "_" + if req.User != nil { + username = req.User.Username + } + switch req.Style { + case web.StyleTweetsFollowing: + key = fmt.Sprintf("%s%s:%d:%d", s.prefixIdxTweetsFollowing, username, offset, limit) + case web.StyleTweetsNewest: + key = fmt.Sprintf("%s%s:%d:%d", s.prefixIdxTweetsNewest, username, offset, limit) + case web.StyleTweetsHots: + key = fmt.Sprintf("%s%s:%d:%d", s.prefixIdxTweetsHots, username, offset, limit) + default: + return + } + if data, err := s.ac.Get(key); err == nil { + ok, res = true, &web.TimelineResp{ + CachePageResp: joint.CachePageResp{ + JsonResp: data, + }, + } + } + return +} + +func (s *looseSrv) tweetCommentsFromCache(req *web.TweetCommentsReq, limit int, offset int) (res *web.TweetCommentsResp, key string, ok bool) { + key = fmt.Sprintf("%s%d:%s:%d:%d", s.prefixTweetComment, req.TweetId, req.Style, limit, offset) + if data, err := s.ac.Get(key); err == nil { + ok, res = true, &web.TweetCommentsResp{ + CachePageResp: joint.CachePageResp{ + JsonResp: data, + }, + } + } + return +} + +func (s *looseSrv) GetUserTweets(req *web.GetUserTweetsReq) (res *web.GetUserTweetsResp, err mir.Error) { + user, xerr := s.RelationTypFrom(req.User, req.Username) + if xerr != nil { + return nil, err + } + // 尝试直接从缓存中获取数据 + key, ok := "", false + if res, key, ok = s.userTweetsFromCache(req, user); ok { + // logrus.Debugf("GetUserTweets from cache key:%s", key) + return + } + // 缓存获取未成功,只能查库了 + switch req.Style { + case web.UserPostsStyleComment, web.UserPostsStyleMedia: + res, err = s.listUserTweets(req, user) + case web.UserPostsStyleHighlight: + res, err = s.getUserPostTweets(req, user, true) + case web.UserPostsStyleStar: + res, err = s.getUserStarTweets(req, user) + case web.UserPostsStylePost: + fallthrough + default: + res, err = s.getUserPostTweets(req, user, false) + } + // 缓存处理 + if err == nil { + base.OnCacheRespEvent(s.ac, key, res.Data, s.userTweetsExpire) + } + return +} + +func (s *looseSrv) userTweetsFromCache(req *web.GetUserTweetsReq, user *cs.VistUser) (res *web.GetUserTweetsResp, key string, ok bool) { + switch req.Style { + case web.UserPostsStylePost, web.UserPostsStyleHighlight, web.UserPostsStyleMedia: + key = fmt.Sprintf("%s%d:%s:%s:%d:%d", s.prefixUserTweets, user.UserId, req.Style, user.RelTyp, req.Page, req.PageSize) + default: + meName := "_" + if user.RelTyp != cs.RelationGuest { + meName = req.User.Username + } + key = fmt.Sprintf("%s%d:%s:%s:%d:%d", s.prefixUserTweets, user.UserId, req.Style, meName, req.Page, req.PageSize) + } + if data, err := s.ac.Get(key); err == nil { + ok, res = true, &web.GetUserTweetsResp{ + CachePageResp: joint.CachePageResp{ + JsonResp: data, + }, + } + } + return +} + +func (s *looseSrv) getUserStarTweets(req *web.GetUserTweetsReq, user *cs.VistUser) (*web.GetUserTweetsResp, mir.Error) { + stars, totalRows, err := s.Ds.ListUserStarTweets(user, req.Limit, req.Offset) + if err != nil { + logrus.Errorf("getUserStarTweets err[1]: %s", err) + return nil, web.ErrGetStarsFailed + } + var posts []*ms.Post + for _, star := range stars { + if star.Post != nil { + posts = append(posts, star.Post) + } + } + postsFormated, err := s.Ds.MergePosts(posts) + if err != nil { + logrus.Errorf("Ds.MergePosts err: %s", err) + return nil, web.ErrGetStarsFailed + } + userId := int64(-1) + if req.User != nil { + userId = req.User.ID + } + if err := s.PrepareTweets(userId, postsFormated); err != nil { + logrus.Errorf("getUserStarTweets err[2]: %s", err) + return nil, web.ErrGetPostsFailed + } + resp := joint.PageRespFrom(postsFormated, req.Page, req.PageSize, totalRows) + return &web.GetUserTweetsResp{ + CachePageResp: joint.CachePageResp{ + Data: resp, + }, + }, nil +} + +func (s *looseSrv) listUserTweets(req *web.GetUserTweetsReq, user *cs.VistUser) (*web.GetUserTweetsResp, mir.Error) { + var ( + tweets []*ms.Post + total int64 + err error + ) + if req.Style == web.UserPostsStyleComment { + tweets, total, err = s.Ds.ListUserCommentTweets(user, req.Limit, req.Offset) + } else if req.Style == web.UserPostsStyleMedia { + tweets, total, err = s.Ds.ListUserMediaTweets(user, req.Limit, req.Offset) + } else { + logrus.Errorf("s.listUserTweets unknow style[1]: %s", req.Style) + return nil, web.ErrGetPostsFailed + } + if err != nil { + logrus.Errorf("s.listUserTweets err[2]: %s", err) + return nil, web.ErrGetPostsFailed + } + postsFormated, err := s.Ds.MergePosts(tweets) + if err != nil { + logrus.Errorf("s.listUserTweets err[3]: %s", err) + return nil, web.ErrGetPostsFailed + } + userId := int64(-1) + if req.User != nil { + userId = req.User.ID + } + if err := s.PrepareTweets(userId, postsFormated); err != nil { + logrus.Errorf("s.listUserTweets err[4]: %s", err) + return nil, web.ErrGetPostsFailed + } + resp := joint.PageRespFrom(postsFormated, req.Page, req.PageSize, total) + return &web.GetUserTweetsResp{ + CachePageResp: joint.CachePageResp{ + Data: resp, + }, + }, nil +} + +func (s *looseSrv) getUserPostTweets(req *web.GetUserTweetsReq, user *cs.VistUser, isHighlight bool) (*web.GetUserTweetsResp, mir.Error) { + style := cs.StyleUserTweetsGuest + switch user.RelTyp { + case cs.RelationAdmin: + style = cs.StyleUserTweetsAdmin + case cs.RelationSelf: + style = cs.StyleUserTweetsSelf + case cs.RelationFriend: + style = cs.StyleUserTweetsFriend + case cs.RelationFollowing: + style = cs.StyleUserTweetsFollowing + case cs.RelationGuest: + fallthrough + default: + // nothing + } + posts, total, err := s.Ds.ListUserTweets(user.UserId, style, isHighlight, req.Limit, req.Offset) + if err != nil { + logrus.Errorf("s.GetTweetList error[1]: %s", err) + return nil, web.ErrGetPostsFailed + } + postsFormated, xerr := s.Ds.MergePosts(posts) + if xerr != nil { + logrus.Errorf("s.GetTweetList error[2]: %s", err) + return nil, web.ErrGetPostsFailed + } + userId := int64(-1) + if req.User != nil { + userId = req.User.ID + } + if err := s.PrepareTweets(userId, postsFormated); err != nil { + logrus.Errorf("s.GetTweetList error[3]: %s", err) + return nil, web.ErrGetPostsFailed + } + resp := joint.PageRespFrom(postsFormated, req.Page, req.PageSize, total) + return &web.GetUserTweetsResp{ + CachePageResp: joint.CachePageResp{ + Data: resp, + }, + }, nil +} + +func (s *looseSrv) GetUserProfile(req *web.GetUserProfileReq) (*web.GetUserProfileResp, mir.Error) { + he, err := s.Ds.UserProfileByName(req.Username) + if err != nil { + logrus.Errorf("looseSrv.GetUserProfile occurs error[1]: %s", err) + return nil, web.ErrNoExistUsername + } + // 设定自己不是自己的朋友 + isFriend := !(req.User == nil || req.User.ID == he.ID) + if req.User != nil && req.User.ID != he.ID { + isFriend = s.Ds.IsFriend(req.User.ID, he.ID) + } + isFollowing := false + if req.User != nil { + isFollowing = s.Ds.IsFollow(req.User.ID, he.ID) + } + follows, followings, err := s.Ds.GetFollowCount(he.ID) + if err != nil { + return nil, web.ErrGetPostsFailed + } + return &web.GetUserProfileResp{ + ID: he.ID, + Nickname: he.Nickname, + Username: he.Username, + Status: he.Status, + Avatar: he.Avatar, + IsAdmin: he.IsAdmin, + IsFriend: isFriend, + IsFollowing: isFollowing, + CreatedOn: he.CreatedOn, + Follows: follows, + Followings: followings, + TweetsCount: he.TweetsCount, + }, nil +} + +func (s *looseSrv) TopicList(req *web.TopicListReq) (*web.TopicListResp, mir.Error) { + var ( + tags, extralTags cs.TagList + err error + ) + num := req.Num + switch req.Type { + case web.TagTypeHot: + tags, err = s.Ds.GetHotTags(req.Uid, num, 0) + case web.TagTypeNew: + tags, err = s.Ds.GetNewestTags(req.Uid, num, 0) + case web.TagTypeFollow: + tags, err = s.Ds.GetFollowTags(req.Uid, false, num, 0) + case web.TagTypePin: + tags, err = s.Ds.GetFollowTags(req.Uid, true, num, 0) + case web.TagTypeHotExtral: + extralNum := req.ExtralNum + if extralNum <= 0 { + extralNum = num + } + tags, err = s.Ds.GetHotTags(req.Uid, num, 0) + if err == nil { + extralTags, err = s.Ds.GetFollowTags(req.Uid, false, extralNum, 0) + } + default: + // TODO: return good error + err = web.ErrGetPostTagsFailed + } + if err != nil { + return nil, web.ErrGetPostTagsFailed + } + return &web.TopicListResp{ + Topics: tags, + ExtralTopics: extralTags, + }, nil +} + +func (s *looseSrv) TweetComments(req *web.TweetCommentsReq) (res *web.TweetCommentsResp, err mir.Error) { + // 尝试直接从缓存中获取数据 + key, ok := "", false + if res, key, ok = s.tweetCommentsFromCache(req, req.Limit, req.Offset); ok { + logrus.Debugf("looseSrv.TweetComments from cache key:%s", key) + return + } + + comments, totalRows, xerr := s.Ds.GetComments(req.TweetId, req.Style.ToInnerValue(), req.Limit, req.Offset) + if xerr != nil { + logrus.Errorf("looseSrv.TweetComments occurs error[1]: %s", xerr) + return nil, web.ErrGetCommentsFailed + } + + userIDs := []int64{} + commentIDs := []int64{} + for _, comment := range comments { + userIDs = append(userIDs, comment.UserID) + commentIDs = append(commentIDs, comment.ID) + } + + users, xerr := s.Ds.GetUsersByIDs(userIDs) + if xerr != nil { + logrus.Errorf("looseSrv.TweetComments occurs error[2]: %s", xerr) + return nil, web.ErrGetCommentsFailed + } + + contents, xerr := s.Ds.GetCommentContentsByIDs(commentIDs) + if xerr != nil { + logrus.Errorf("looseSrv.TweetComments occurs error[3]: %s", xerr) + return nil, web.ErrGetCommentsFailed + } + + replies, xerr := s.Ds.GetCommentRepliesByID(commentIDs) + if xerr != nil { + logrus.Errorf("looseSrv.TweetComments occurs error[4]: %s", xerr) + return nil, web.ErrGetCommentsFailed + } + + var commentThumbs, replyThumbs cs.CommentThumbsMap + if req.Uid > 0 { + commentThumbs, replyThumbs, xerr = s.Ds.GetCommentThumbsMap(req.Uid, req.TweetId) + if xerr != nil { + logrus.Errorf("looseSrv.TweetComments occurs error[5]: %s", xerr) + return nil, web.ErrGetCommentsFailed + } + } + + replyMap := make(map[int64][]*dbr.CommentReplyFormated) + if len(replyThumbs) > 0 { + for _, reply := range replies { + if thumbs, exist := replyThumbs[reply.ID]; exist { + reply.IsThumbsUp, reply.IsThumbsDown = thumbs.IsThumbsUp, thumbs.IsThumbsDown + } + replyMap[reply.CommentID] = append(replyMap[reply.CommentID], reply) + } + } else { + for _, reply := range replies { + replyMap[reply.CommentID] = append(replyMap[reply.CommentID], reply) + } + } + + commentsFormated := []*ms.CommentFormated{} + for _, comment := range comments { + commentFormated := comment.Format() + if thumbs, exist := commentThumbs[comment.ID]; exist { + commentFormated.IsThumbsUp, commentFormated.IsThumbsDown = thumbs.IsThumbsUp, thumbs.IsThumbsDown + } + for _, content := range contents { + if content.CommentID == comment.ID { + commentFormated.Contents = append(commentFormated.Contents, content) + } + } + if replySlice, exist := replyMap[commentFormated.ID]; exist { + commentFormated.Replies = replySlice + } + for _, user := range users { + if user.ID == comment.UserID { + commentFormated.User = user.Format() + } + } + commentsFormated = append(commentsFormated, commentFormated) + } + resp := joint.PageRespFrom(commentsFormated, req.Page, req.PageSize, totalRows) + // 缓存处理 + base.OnCacheRespEvent(s.ac, key, resp, s.tweetCommentsExpire) + return &web.TweetCommentsResp{ + CachePageResp: joint.CachePageResp{ + Data: resp, + }, + }, nil +} + +func (s *looseSrv) TweetDetail(req *web.TweetDetailReq) (*web.TweetDetailResp, mir.Error) { + post, err := s.Ds.GetPostByID(req.TweetId) + if err != nil { + return nil, web.ErrGetPostFailed + } + postContents, err := s.Ds.GetPostContentsByIDs([]int64{post.ID}) + if err != nil { + return nil, web.ErrGetPostFailed + } + users, err := s.Ds.GetUsersByIDs([]int64{post.UserID}) + if err != nil { + return nil, web.ErrGetPostFailed + } + // 数据整合 + postFormated := post.Format() + for _, user := range users { + postFormated.User = user.Format() + } + for _, content := range postContents { + if content.PostID == post.ID { + postFormated.Contents = append(postFormated.Contents, content.Format()) + } + } + if err = s.PrepareTweet(req.User, postFormated); err != nil { + return nil, web.ErrGetPostFailed + } + // 检测访问权限 + // TODO: 提到最前面去检测 + switch { + case req.User != nil && (req.User.ID == postFormated.User.ID || req.User.IsAdmin): + // read by self of super admin + break + case post.Visibility == core.PostVisitPublic: + break + case post.Visibility == core.PostVisitFriend && postFormated.User.IsFriend: + break + case post.Visibility == core.PostVisitFollowing && postFormated.User.IsFollowing: + break + default: + return nil, web.ErrNoPermission + } + return (*web.TweetDetailResp)(postFormated), nil +} + +func newLooseSrv(s *base.DaoServant, ac core.AppCache) api.Loose { + cs := conf.CacheSetting + return &looseSrv{ + DaoServant: s, + ac: ac, + userTweetsExpire: cs.UserTweetsExpire, + idxTweetsExpire: cs.IndexTweetsExpire, + tweetCommentsExpire: cs.TweetCommentsExpire, + prefixUserTweets: conf.PrefixUserTweets, + prefixIdxTweetsNewest: conf.PrefixIdxTweetsNewest, + prefixIdxTweetsHots: conf.PrefixIdxTweetsHots, + prefixIdxTweetsFollowing: conf.PrefixIdxTweetsFollowing, + prefixTweetComment: conf.PrefixTweetComment, + } +} diff --git a/internal/servants/web/v2/pub.go b/internal/servants/web/v2/pub.go new file mode 100644 index 000000000..fd4328efd --- /dev/null +++ b/internal/servants/web/v2/pub.go @@ -0,0 +1,194 @@ +// Copyright 2025 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package web + +import ( + "bytes" + "context" + "encoding/base64" + "image/color" + "image/png" + "regexp" + "unicode/utf8" + + "github.com/afocus/captcha" + "github.com/alimy/mir/v4" + "github.com/gofrs/uuid/v5" + api "github.com/rocboss/paopao-ce/auto/api/v2" + "github.com/rocboss/paopao-ce/internal/core/ms" + "github.com/rocboss/paopao-ce/internal/model/web" + "github.com/rocboss/paopao-ce/internal/servants/base" + "github.com/rocboss/paopao-ce/internal/servants/web/assets" + "github.com/rocboss/paopao-ce/pkg/app" + "github.com/rocboss/paopao-ce/pkg/utils" + "github.com/rocboss/paopao-ce/pkg/version" + "github.com/rocboss/paopao-ce/pkg/xerror" + "github.com/sirupsen/logrus" +) + +var ( + _ api.Pub = (*pubSrv)(nil) +) + +const ( + _MaxLoginErrTimes = 10 + _MaxPhoneCaptcha = 10 +) + +type pubSrv struct { + api.UnimplementedPubServant + *base.DaoServant +} + +func (s *pubSrv) SendCaptcha(req *web.SendCaptchaReq) mir.Error { + ctx := context.Background() + + // 验证图片验证码 + if captcha, err := s.Redis.GetImgCaptcha(ctx, req.ImgCaptchaID); err != nil || string(captcha) != req.ImgCaptcha { + logrus.Debugf("get captcha err:%s expect:%s got:%s", err, captcha, req.ImgCaptcha) + return web.ErrErrorCaptchaPassword + } + s.Redis.DelImgCaptcha(ctx, req.ImgCaptchaID) + + // 今日频次限制 + if count, _ := s.Redis.GetCountSmsCaptcha(ctx, req.Phone); count >= _MaxPhoneCaptcha { + return web.ErrTooManyPhoneCaptchaSend + } + + if err := s.Ds.SendPhoneCaptcha(req.Phone); err != nil { + return xerror.ServerError + } + // 写入计数缓存 + s.Redis.IncrCountSmsCaptcha(ctx, req.Phone) + + return nil +} + +func (s *pubSrv) GetCaptcha() (*web.GetCaptchaResp, mir.Error) { + cap := captcha.New() + if err := cap.AddFontFromBytes(assets.ComicBytes); err != nil { + logrus.Errorf("cap.AddFontFromBytes err:%s", err) + return nil, xerror.ServerError + } + cap.SetSize(160, 64) + cap.SetDisturbance(captcha.MEDIUM) + cap.SetFrontColor(color.RGBA{0, 0, 0, 255}) + cap.SetBkgColor(color.RGBA{218, 240, 228, 255}) + img, password := cap.Create(6, captcha.NUM) + emptyBuff := bytes.NewBuffer(nil) + if err := png.Encode(emptyBuff, img); err != nil { + logrus.Errorf("png.Encode err:%s", err) + return nil, xerror.ServerError + } + key := utils.EncodeMD5(uuid.Must(uuid.NewV4()).String()) + // 五分钟有效期 + s.Redis.SetImgCaptcha(context.Background(), key, password) + return &web.GetCaptchaResp{ + Id: key, + Content: "data:image/png;base64," + base64.StdEncoding.EncodeToString(emptyBuff.Bytes()), + }, nil +} + +func (s *pubSrv) Register(req *web.RegisterReq) (*web.RegisterResp, mir.Error) { + if _disallowUserRegister { + return nil, web.ErrDisallowUserRegister + } + // 用户名检查 + if err := s.validUsername(req.Username); err != nil { + return nil, err + } + // 密码检查 + if err := checkPassword(req.Password); err != nil { + logrus.Errorf("scheckPassword err: %v", err) + return nil, web.ErrUserRegisterFailed + } + password, salt := encryptPasswordAndSalt(req.Password) + user := &ms.User{ + Nickname: req.Username, + Username: req.Username, + Password: password, + Avatar: getRandomAvatar(), + Salt: salt, + Status: ms.UserStatusNormal, + } + user, err := s.Ds.CreateUser(user) + if err != nil { + logrus.Errorf("Ds.CreateUser err: %s", err) + return nil, web.ErrUserRegisterFailed + } + return &web.RegisterResp{ + UserId: user.ID, + Username: user.Username, + }, nil +} + +func (s *pubSrv) Login(req *web.LoginReq) (*web.LoginResp, mir.Error) { + ctx := context.Background() + user, err := s.Ds.GetUserByUsername(req.Username) + if err != nil { + logrus.Errorf("Ds.GetUserByUsername err:%s", err) + return nil, xerror.UnauthorizedAuthNotExist + } + + if user.Model != nil && user.ID > 0 { + if count, err := s.Redis.GetCountLoginErr(ctx, user.ID); err == nil && count >= _MaxLoginErrTimes { + return nil, web.ErrTooManyLoginError + } + // 对比密码是否正确 + if validPassword(user.Password, req.Password, user.Salt) { + if user.Status == ms.UserStatusClosed { + return nil, web.ErrUserHasBeenBanned + } + // 清空登录计数 + s.Redis.DelCountLoginErr(ctx, user.ID) + } else { + // 登录错误计数 + s.Redis.IncrCountLoginErr(ctx, user.ID) + return nil, xerror.UnauthorizedAuthFailed + } + } else { + return nil, xerror.UnauthorizedAuthNotExist + } + + token, err := app.GenerateToken(user) + if err != nil { + logrus.Errorf("app.GenerateToken err: %v", err) + return nil, xerror.UnauthorizedTokenGenerate + } + return &web.LoginResp{ + Token: token, + }, nil +} + +func (s *pubSrv) Version() (*web.VersionResp, mir.Error) { + return &web.VersionResp{ + BuildInfo: version.ReadBuildInfo(), + }, nil +} + +// validUsername 验证用户 +func (s *pubSrv) validUsername(username string) mir.Error { + // 检测用户是否合规 + if utf8.RuneCountInString(username) < 3 || utf8.RuneCountInString(username) > 12 { + return web.ErrUsernameLengthLimit + } + + if !regexp.MustCompile(`^[a-zA-Z0-9]+$`).MatchString(username) { + return web.ErrUsernameCharLimit + } + + // 重复检查 + user, _ := s.Ds.GetUserByUsername(username) + if user.Model != nil && user.ID > 0 { + return web.ErrUsernameHasExisted + } + return nil +} + +func newPubSrv(s *base.DaoServant) api.Pub { + return &pubSrv{ + DaoServant: s, + } +} diff --git a/internal/servants/web/v2/utils.go b/internal/servants/web/v2/utils.go new file mode 100644 index 000000000..888cc6450 --- /dev/null +++ b/internal/servants/web/v2/utils.go @@ -0,0 +1,236 @@ +// Copyright 2025 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package web + +import ( + "image" + "math/rand" + "strings" + "time" + "unicode/utf8" + + "github.com/alimy/mir/v4" + "github.com/gofrs/uuid/v5" + "github.com/rocboss/paopao-ce/internal/core" + "github.com/rocboss/paopao-ce/internal/core/ms" + "github.com/rocboss/paopao-ce/internal/model/web" + "github.com/rocboss/paopao-ce/pkg/utils" + "github.com/rocboss/paopao-ce/pkg/xerror" + "github.com/sirupsen/logrus" +) + +var defaultAvatars = []string{ + "https://assets.paopao.info/public/avatar/default/zoe.png", + "https://assets.paopao.info/public/avatar/default/william.png", + "https://assets.paopao.info/public/avatar/default/walter.png", + "https://assets.paopao.info/public/avatar/default/thomas.png", + "https://assets.paopao.info/public/avatar/default/taylor.png", + "https://assets.paopao.info/public/avatar/default/sophia.png", + "https://assets.paopao.info/public/avatar/default/sam.png", + "https://assets.paopao.info/public/avatar/default/ryan.png", + "https://assets.paopao.info/public/avatar/default/ruby.png", + "https://assets.paopao.info/public/avatar/default/quinn.png", + "https://assets.paopao.info/public/avatar/default/paul.png", + "https://assets.paopao.info/public/avatar/default/owen.png", + "https://assets.paopao.info/public/avatar/default/olivia.png", + "https://assets.paopao.info/public/avatar/default/norman.png", + "https://assets.paopao.info/public/avatar/default/nora.png", + "https://assets.paopao.info/public/avatar/default/natalie.png", + "https://assets.paopao.info/public/avatar/default/naomi.png", + "https://assets.paopao.info/public/avatar/default/miley.png", + "https://assets.paopao.info/public/avatar/default/mike.png", + "https://assets.paopao.info/public/avatar/default/lucas.png", + "https://assets.paopao.info/public/avatar/default/kylie.png", + "https://assets.paopao.info/public/avatar/default/julia.png", + "https://assets.paopao.info/public/avatar/default/joshua.png", + "https://assets.paopao.info/public/avatar/default/john.png", + "https://assets.paopao.info/public/avatar/default/jane.png", + "https://assets.paopao.info/public/avatar/default/jackson.png", + "https://assets.paopao.info/public/avatar/default/ivy.png", + "https://assets.paopao.info/public/avatar/default/isaac.png", + "https://assets.paopao.info/public/avatar/default/henry.png", + "https://assets.paopao.info/public/avatar/default/harry.png", + "https://assets.paopao.info/public/avatar/default/harold.png", + "https://assets.paopao.info/public/avatar/default/hanna.png", + "https://assets.paopao.info/public/avatar/default/grace.png", + "https://assets.paopao.info/public/avatar/default/george.png", + "https://assets.paopao.info/public/avatar/default/freddy.png", + "https://assets.paopao.info/public/avatar/default/frank.png", + "https://assets.paopao.info/public/avatar/default/finn.png", + "https://assets.paopao.info/public/avatar/default/emma.png", + "https://assets.paopao.info/public/avatar/default/emily.png", + "https://assets.paopao.info/public/avatar/default/edward.png", + "https://assets.paopao.info/public/avatar/default/clara.png", + "https://assets.paopao.info/public/avatar/default/claire.png", + "https://assets.paopao.info/public/avatar/default/chloe.png", + "https://assets.paopao.info/public/avatar/default/audrey.png", + "https://assets.paopao.info/public/avatar/default/arthur.png", + "https://assets.paopao.info/public/avatar/default/anna.png", + "https://assets.paopao.info/public/avatar/default/andy.png", + "https://assets.paopao.info/public/avatar/default/alfred.png", + "https://assets.paopao.info/public/avatar/default/alexa.png", + "https://assets.paopao.info/public/avatar/default/abigail.png", +} + +func getRandomAvatar() string { + rand.Seed(time.Now().UnixMicro()) + return defaultAvatars[rand.Intn(len(defaultAvatars))] +} + +// checkPassword 密码检查 +func checkPassword(password string) mir.Error { + // 检测用户是否合规 + if utf8.RuneCountInString(password) < 6 || utf8.RuneCountInString(password) > 16 { + return web.ErrPasswordLengthLimit + } + return nil +} + +// ValidPassword 检查密码是否一致 +func validPassword(dbPassword, password, salt string) bool { + return strings.Compare(dbPassword, utils.EncodeMD5(utils.EncodeMD5(password)+salt)) == 0 +} + +// encryptPasswordAndSalt 密码加密&生成salt +func encryptPasswordAndSalt(password string) (string, string) { + salt := uuid.Must(uuid.NewV4()).String()[:8] + password = utils.EncodeMD5(utils.EncodeMD5(password) + salt) + return password, salt +} + +// deleteOssObjects 删除推文的媒体内容, 宽松处理错误(就是不处理), 后续完善 +func deleteOssObjects(oss core.ObjectStorageService, mediaContents []string) { + mediaContentsSize := len(mediaContents) + if mediaContentsSize > 1 { + objectKeys := make([]string, 0, mediaContentsSize) + for _, cUrl := range mediaContents { + objectKeys = append(objectKeys, oss.ObjectKey(cUrl)) + } + // TODO: 优化处理尽量使用channel传递objectKeys使用可控数量的Goroutine集中处理object删除动作,后续完善 + go oss.DeleteObjects(objectKeys) + } else if mediaContentsSize == 1 { + oss.DeleteObject(oss.ObjectKey(mediaContents[0])) + } +} + +// persistMediaContents 获取媒体内容并持久化 +func persistMediaContents(oss core.ObjectStorageService, contents []*web.PostContentItem) (items []string, err error) { + items = make([]string, 0, len(contents)) + for _, item := range contents { + switch item.Type { + case ms.ContentTypeImage, + ms.ContentTypeVideo, + ms.ContentTypeAudio, + ms.ContentTypeAttachment, + ms.ContentTypeChargeAttachment: + items = append(items, item.Content) + if err != nil { + continue + } + if err = oss.PersistObject(oss.ObjectKey(item.Content)); err != nil { + logrus.Errorf("service.persistMediaContents failed: %s", err) + } + } + } + return +} + +func fileCheck(uploadType string, size int64) mir.Error { + if uploadType != "public/video" && + uploadType != "public/image" && + uploadType != "public/avatar" && + uploadType != "attachment" { + return xerror.InvalidParams + } + if size > 1024*1024*100 { + return web.ErrFileInvalidSize.WithDetails("最大允许100MB") + } + return nil +} + +func getFileExt(s string) (string, mir.Error) { + switch s { + case "image/png": + return ".png", nil + case "image/jpg": + return ".jpg", nil + case "image/jpeg": + return ".jpeg", nil + case "image/gif": + return ".gif", nil + case "video/mp4": + return ".mp4", nil + case "video/quicktime": + return ".mov", nil + case "application/zip", + "application/x-zip", + "application/octet-stream", + "application/x-zip-compressed": + return ".zip", nil + default: + return "", web.ErrFileInvalidExt.WithDetails("仅允许 png/jpg/gif/mp4/mov/zip 类型") + } +} + +func generatePath(s string) string { + n := len(s) + if n <= 2 { + return s + } + return generatePath(s[:n-2]) + "/" + s[n-2:] +} + +func getImageSize(img image.Rectangle) (int, int) { + b := img.Bounds() + width := b.Max.X + height := b.Max.Y + return width, height +} + +func tagsFrom(originTags []string) []string { + tags := make([]string, 0, len(originTags)) + for _, tag := range originTags { + // TODO: 优化tag有效性检测 + if tag = strings.TrimSpace(tag); len(tag) > 0 { + tags = append(tags, tag) + } + } + return tags +} + +// checkPermision 检查是否拥有者或管理员 +func checkPermision(user *ms.User, targetUserId int64) mir.Error { + if user == nil || (user.ID != targetUserId && !user.IsAdmin) { + return web.ErrNoPermission + } + return nil +} + +// checkPostViewPermission 检查当前用户是否可读指定post +func checkPostViewPermission(user *ms.User, post *ms.Post, ds core.DataService) mir.Error { + if post.Visibility == core.PostVisitPublic { + return nil + } + + if user == nil { + return web.ErrNoPermission + } + + if user.IsAdmin || user.ID == post.UserID { + return nil + } + + if post.Visibility == core.PostVisitPrivate { + return web.ErrNoPermission + } + + if post.Visibility == core.PostVisitFriend { + if !ds.IsFriend(post.UserID, user.ID) && !ds.IsFriend(user.ID, post.UserID) { + return web.ErrNoPermission + } + } + // TODO: add following check logic + return nil +} diff --git a/internal/servants/web/v2/web.go b/internal/servants/web/v2/web.go new file mode 100644 index 000000000..82bc39177 --- /dev/null +++ b/internal/servants/web/v2/web.go @@ -0,0 +1,48 @@ +// Copyright 2025 ROC. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package web + +import ( + "sync" + + "github.com/alimy/tryst/cfg" + "github.com/gin-gonic/gin" + api "github.com/rocboss/paopao-ce/auto/api/v2" + "github.com/rocboss/paopao-ce/internal/core" + "github.com/rocboss/paopao-ce/internal/dao" + "github.com/rocboss/paopao-ce/internal/dao/cache" + "github.com/rocboss/paopao-ce/internal/servants/base" +) + +var ( + _enablePhoneVerify bool + _disallowUserRegister bool + _ds core.DataService + _ac core.AppCache + _wc core.WebCache + _oss core.ObjectStorageService + _onceInitial sync.Once +) + +// RouteWeb register web route +func RouteWeb(e *gin.Engine) { + lazyInitial() + ds := base.NewDaoServant() + // aways register servants + api.RegisterLooseServant(e, newLooseSrv(ds, _ac)) + api.RegisterPubServant(e, newPubSrv(ds)) +} + +// lazyInitial do some package lazy initialize for performance +func lazyInitial() { + _onceInitial.Do(func() { + _enablePhoneVerify = cfg.If("Sms") + _disallowUserRegister = cfg.If("Web:DisallowUserRegister") + _oss = dao.ObjectStorageService() + _ds = dao.DataService() + _ac = cache.NewAppCache() + _wc = cache.NewWebCache() + }) +} diff --git a/mirc/gen.go b/mirc/gen.go index dcd402ac5..4d6a72fad 100644 --- a/mirc/gen.go +++ b/mirc/gen.go @@ -19,6 +19,7 @@ import ( _ "github.com/rocboss/paopao-ce/mirc/localoss/v1" _ "github.com/rocboss/paopao-ce/mirc/space/v1" _ "github.com/rocboss/paopao-ce/mirc/web/v1" + _ "github.com/rocboss/paopao-ce/mirc/web/v2" ) //go:generate go run $GOFILE diff --git a/mirc/web/README.md b/mirc/web/README.md index f86907126..6fd540333 100644 --- a/mirc/web/README.md +++ b/mirc/web/README.md @@ -1,4 +1,4 @@ ### Web系列RESTful API 本目录包含Web系列RESTful API相关定义文件 -* v1 - v1版本API +* v2 - v2版本API diff --git a/mirc/web/v2/loose.go b/mirc/web/v2/loose.go new file mode 100644 index 000000000..7d93d2f39 --- /dev/null +++ b/mirc/web/v2/loose.go @@ -0,0 +1,35 @@ +package v1 + +import ( + . "github.com/alimy/mir/v4" + . "github.com/alimy/mir/v4/engine" + "github.com/rocboss/paopao-ce/internal/model/v2/web" +) + +func init() { + Entry[Loose]() +} + +// Loose 宽松授权的服务 +type Loose struct { + Chain `mir:"-"` + Group `mir:"v2"` + + // Timeline 获取广场流 + Timeline func(Get, web.TimelineReq) web.TimelineResp `mir:"posts" binding:"query"` + + // GetUserTweets 获取用户动态列表 + GetUserTweets func(Get, web.GetUserTweetsReq) web.GetUserTweetsResp `mir:"user/posts" binding:"query"` + + // GetUserProfile 获取用户基本信息 + GetUserProfile func(Get, web.GetUserProfileReq) web.GetUserProfileResp `mir:"user/profile" binding:"query"` + + // TopicList 获取话题列表 + TopicList func(Get, web.TopicListReq) web.TopicListResp `mir:"tags" binding:"query"` + + // TweetComments 获取动态评论 + TweetComments func(Get, web.TweetCommentsReq) web.TweetCommentsResp `mir:"post/comments" binding:"query"` + + // TweetDetail 获取动态详情 + TweetDetail func(Get, web.TweetDetailReq) web.TweetDetailResp `mir:"post" binding:"query"` +} diff --git a/mirc/web/v2/pub.go b/mirc/web/v2/pub.go new file mode 100644 index 000000000..a043aa6be --- /dev/null +++ b/mirc/web/v2/pub.go @@ -0,0 +1,31 @@ +package v1 + +import ( + . "github.com/alimy/mir/v4" + . "github.com/alimy/mir/v4/engine" + "github.com/rocboss/paopao-ce/internal/model/web" +) + +func init() { + Entry[Pub]() +} + +// Pub 不用授权的公开服务 +type Pub struct { + Group `mir:"v2"` + + // Version 获取后台版本信息 + Version func(Get) web.VersionResp `mir:"/"` + + // Login 用户登录 + Login func(Post, web.LoginReq) web.LoginResp `mir:"auth/login"` + + // Register 用户注册 + Register func(Post, web.RegisterReq) web.RegisterResp `mir:"auth/register"` + + // GetCaptcha 获取验证码 + GetCaptcha func(Get) web.GetCaptchaResp `mir:"captcha"` + + // SendCaptcha 发送验证码 + SendCaptcha func(Post, web.SendCaptchaReq) `mir:"captcha"` +} diff --git a/pkg/app/pagination.go b/pkg/app/pagination.go index 09d7532b4..13d3e6896 100644 --- a/pkg/app/pagination.go +++ b/pkg/app/pagination.go @@ -60,3 +60,16 @@ func GetPageInfo(c *gin.Context) (page, pageSize int) { } return } + +func LimitOffset(page, size int) (limit, offset int) { + if page <= 0 { + page = 1 + } + if size <= 0 { + limit = conf.AppSetting.DefaultPageSize + } else if size > conf.AppSetting.MaxPageSize { + limit = conf.AppSetting.MaxPageSize + } + offset = (page - 1) * limit + return +}