diff --git a/README.md b/README.md index 819961d..5de1973 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,10 @@ mv order.proto ../../rpc/order/ 编辑 `rpc/order/order.proto`,把 `go_package` 改成本仓库模块路径: ```protobuf + +import "google/api/annotations.proto"; option go_package = "lone-services/rpc/order"; + ``` ### 3. 生成pb 文件 diff --git a/deploy/apisix/lua/auth.lua b/deploy/apisix/lua/auth.lua index 4a48ae1..4b3a5dc 100644 --- a/deploy/apisix/lua/auth.lua +++ b/deploy/apisix/lua/auth.lua @@ -11,18 +11,20 @@ function _M.access(conf, ctx) core.log.info("REQUEST_DEBUG: uri = "..request_uri) local white_list = { - ["/api/v3/sms/send"] = true, - ["/api/v3/login"] = true, - ["/api/v3/version"] = true, + ["/api/v3/sms/send"] = true, + ["/api/v3/login"] = true, + ["/api/v3/version"] = true, - ["/admin/v3/login"] = true, - ["/admin/v3/refresh"] = true, - ["/admin/v3/transit/bind"] = true, + ["/admin/v3/login"] = true, + ["/admin/v3/refresh"] = true, + ["/admin/v3/transit/bind"] = true, - ["/api/v3/express/sf"] = true, - ["/api/v3/express/jd"] = true, + ["/api/v3/express/sf"] = true, + ["/api/v3/express/jd"] = true, ["/api/v3/chore/oss/upload"] = true, + + ["/api/v2/analysis/qrcode/generate"] = true, } local user_key = core.request.header(ctx, "authorization") diff --git a/pkg/oss/oss.go b/pkg/oss/oss.go index 1beb3bc..4201770 100644 --- a/pkg/oss/oss.go +++ b/pkg/oss/oss.go @@ -124,7 +124,6 @@ func normalizeEndpoint(endpoint, bucket string) string { return ep } -// buildObjectKey: {oss.dir}{YYYYMMDD}/{filename} func buildObjectKey(configDir, filename string) string { base := strings.Trim(sanitizeDir(configDir), "/") if base != "" { diff --git a/pkg/wechat/auth/token.go b/pkg/wechat/auth/token.go index 21078dc..b14a7ed 100644 --- a/pkg/wechat/auth/token.go +++ b/pkg/wechat/auth/token.go @@ -1,8 +1,13 @@ package auth import ( + "context" + "crypto/md5" + "encoding/hex" "fmt" + "time" + "lone-services/pkg/redis" "lone-services/pkg/utils" jsoniter "github.com/json-iterator/go" @@ -17,10 +22,21 @@ type AccessTokenResult struct { ErrMsg string `json:"errmsg"` } -func (c *Client) AccessToken() (*AccessTokenResult, error) { +func (c *Client) AccessToken(ctx context.Context) (*AccessTokenResult, error) { if c == nil || c.AppID == utils.StringEmpty || c.Secret == utils.StringEmpty { return nil, fmt.Errorf("wechat app config missing") } + if ctx == nil { + ctx = context.Background() + } + + secretMd5 := md5.Sum([]byte(c.Secret)) + key := fmt.Sprintf("wechat:access_token:%s:%s", c.AppID, hex.EncodeToString(secretMd5[:])[:8]) + + token, err := redis.Client.Get(ctx, key).Result() + if err == nil && token != utils.StringEmpty { + return &AccessTokenResult{AccessToken: token}, nil + } body, err := utils.Curl(accessTokenURL).SetData(map[string]string{ "grant_type": "client_credential", @@ -41,5 +57,10 @@ func (c *Client) AccessToken() (*AccessTokenResult, error) { if result.AccessToken == utils.StringEmpty { return nil, fmt.Errorf("wechat access_token empty") } + + expire := time.Duration(result.ExpiresIn-300) * time.Second + if expire > 0 { + _ = redis.Client.Set(ctx, key, result.AccessToken, expire).Err() + } return &result, nil } diff --git a/pkg/wechat/qrcode/client.go b/pkg/wechat/qrcode/client.go new file mode 100644 index 0000000..6a3e333 --- /dev/null +++ b/pkg/wechat/qrcode/client.go @@ -0,0 +1,13 @@ +package qrcode + +type Client struct { + AppID string + Secret string +} + +func NewClient(appID, secret string) *Client { + return &Client{ + AppID: appID, + Secret: secret, + } +} diff --git a/pkg/wechat/qrcode/types.go b/pkg/wechat/qrcode/types.go new file mode 100644 index 0000000..7527cf3 --- /dev/null +++ b/pkg/wechat/qrcode/types.go @@ -0,0 +1,33 @@ +package qrcode + +type LineColor struct { + R int `json:"r"` + G int `json:"g"` + B int `json:"b"` +} + +type UnlimitedRequest struct { + Scene string `json:"scene"` + Page string `json:"page,omitempty"` + CheckPath *bool `json:"check_path,omitempty"` + EnvVersion string `json:"env_version,omitempty"` // release / trial / develop + Width int `json:"width,omitempty"` + AutoColor bool `json:"auto_color,omitempty"` + LineColor *LineColor `json:"line_color,omitempty"` + IsHyaline bool `json:"is_hyaline,omitempty"` +} + +// WxaCodeRequest 有数量限制的小程序码(getwxacode),path 可带 query。 +type WxaCodeRequest struct { + Path string `json:"path"` + EnvVersion string `json:"env_version,omitempty"` + Width int `json:"width,omitempty"` + AutoColor bool `json:"auto_color,omitempty"` + LineColor *LineColor `json:"line_color,omitempty"` + IsHyaline bool `json:"is_hyaline,omitempty"` +} + +type apiErrorBody struct { + ErrCode int `json:"errcode"` + ErrMsg string `json:"errmsg"` +} diff --git a/pkg/wechat/qrcode/unlimited.go b/pkg/wechat/qrcode/unlimited.go new file mode 100644 index 0000000..d9ab7d6 --- /dev/null +++ b/pkg/wechat/qrcode/unlimited.go @@ -0,0 +1,88 @@ +package qrcode + +import ( + "bytes" + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "time" + + "lone-services/pkg/utils" + "lone-services/pkg/wechat/auth" +) + +const unlimitedURL = "https://api.weixin.qq.com/wxa/getwxacodeunlimit" + +func (c *Client) GetUnlimited(accessToken string, req *UnlimitedRequest) ([]byte, error) { + if accessToken == utils.StringEmpty { + return nil, fmt.Errorf("wechat access_token empty") + } + if req == nil || req.Scene == utils.StringEmpty { + return nil, fmt.Errorf("wechat qrcode scene empty") + } + + payload, err := json.Marshal(req) + if err != nil { + return nil, err + } + + url := fmt.Sprintf("%s?access_token=%s", unlimitedURL, accessToken) + httpClient := &http.Client{Timeout: 15 * time.Second} + resp, err := httpClient.Post(url, "application/json", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("wechat qrcode http status %d", resp.StatusCode) + } + + if err := parseImageOrError(body); err != nil { + return nil, err + } + return body, nil +} + +func (c *Client) CreateUnlimited(ctx context.Context, req *UnlimitedRequest) ([]byte, error) { + token, err := auth.NewClient(c.AppID, c.Secret).AccessToken(ctx) + if err != nil { + return nil, err + } + return c.GetUnlimited(token.AccessToken, req) +} + +func (c *Client) CreateUnlimitedBase64(ctx context.Context, req *UnlimitedRequest) (string, error) { + img, err := c.CreateUnlimited(ctx, req) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(img), nil +} + +func parseImageOrError(body []byte) error { + trim := bytes.TrimSpace(body) + if len(trim) == 0 { + return fmt.Errorf("wechat qrcode empty response") + } + if trim[0] == '{' || strings.HasPrefix(string(trim), "{\"errcode\"") { + var errBody apiErrorBody + if err := json.Unmarshal(trim, &errBody); err != nil { + return fmt.Errorf("wechat qrcode invalid error body: %w", err) + } + if errBody.ErrCode != utils.NumberZero { + return errors.New(errBody.ErrMsg) + } + return fmt.Errorf("wechat qrcode unexpected json response") + } + return nil +} diff --git a/pkg/wechat/qrcode/wxacode.go b/pkg/wechat/qrcode/wxacode.go new file mode 100644 index 0000000..e42d855 --- /dev/null +++ b/pkg/wechat/qrcode/wxacode.go @@ -0,0 +1,75 @@ +package qrcode + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "time" + + "lone-services/pkg/utils" + "lone-services/pkg/wechat/auth" +) + +// 小程序码(有数量限制):https://developers.weixin.qq.com/miniprogram/dev/OpenApiDoc/qrcode-link/qr-code/getQRCode.html +const wxaCodeURL = "https://api.weixin.qq.com/wxa/getwxacode" + +// BuildPath 拼小程序 path,如 pagesB/userLogin/index?session_id=xxx +func BuildPath(page string, params map[string]string) string { + if len(params) == 0 { + return page + } + query := url.Values{} + for k, v := range params { + query.Add(k, v) + } + return page + "?" + query.Encode() +} + +// GetWxaCode 使用已有 access_token 生成有限小程序码,成功返回图片二进制。 +func (c *Client) GetWxaCode(accessToken string, req *WxaCodeRequest) ([]byte, error) { + if accessToken == utils.StringEmpty { + return nil, fmt.Errorf("wechat access_token empty") + } + if req == nil || req.Path == utils.StringEmpty { + return nil, fmt.Errorf("wechat qrcode path empty") + } + + payload, err := json.Marshal(req) + if err != nil { + return nil, err + } + + apiURL := fmt.Sprintf("%s?access_token=%s", wxaCodeURL, accessToken) + httpClient := &http.Client{Timeout: 15 * time.Second} + resp, err := httpClient.Post(apiURL, "application/json", bytes.NewReader(payload)) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("wechat qrcode http status %d", resp.StatusCode) + } + + if err := parseImageOrError(body); err != nil { + return nil, err + } + return body, nil +} + +// CreateWxaCode 通过 auth 拉取 access_token 后生成有限小程序码。 +func (c *Client) CreateWxaCode(ctx context.Context, req *WxaCodeRequest) ([]byte, error) { + token, err := auth.NewClient(c.AppID, c.Secret).AccessToken(ctx) + if err != nil { + return nil, err + } + return c.GetWxaCode(token.AccessToken, req) +} diff --git a/rpc/analysis/analysis.pb b/rpc/analysis/analysis.pb index 048a7ca..03dafbe 100644 Binary files a/rpc/analysis/analysis.pb and b/rpc/analysis/analysis.pb differ diff --git a/rpc/analysis/analysis.proto b/rpc/analysis/analysis.proto index 0181e6f..767ba1a 100644 --- a/rpc/analysis/analysis.proto +++ b/rpc/analysis/analysis.proto @@ -1,16 +1,23 @@ syntax = "proto3"; package analysis; +import "google/api/annotations.proto"; option go_package = "lone-services/rpc/analysis"; -message Request { - string ping = 1; +service AnalysisAuth { + rpc GenerateQrcode(GenerateQrcodeRequest) returns(Response){ + option (google.api.http) = { + post: "/api/v2/analysis/qrcode/generate" + body: "*" + }; + }; +} + +message GenerateQrcodeRequest { } message Response { - string pong = 1; -} - -service Analysis { - rpc Ping(Request) returns(Response); + int32 code = 1; + string msg = 2; + string data = 3; } diff --git a/rpc/analysis/pb/analysis.pb.go b/rpc/analysis/pb/analysis.pb.go index 98b2122..7c2c7b8 100644 --- a/rpc/analysis/pb/analysis.pb.go +++ b/rpc/analysis/pb/analysis.pb.go @@ -7,6 +7,7 @@ package analysis import ( + _ "google.golang.org/genproto/googleapis/api/annotations" protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" @@ -21,27 +22,26 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) -type Request struct { +type GenerateQrcodeRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - Ping string `protobuf:"bytes,1,opt,name=ping,proto3" json:"ping,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } -func (x *Request) Reset() { - *x = Request{} +func (x *GenerateQrcodeRequest) Reset() { + *x = GenerateQrcodeRequest{} mi := &file_analysis_analysis_proto_msgTypes[0] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } -func (x *Request) String() string { +func (x *GenerateQrcodeRequest) String() string { return protoimpl.X.MessageStringOf(x) } -func (*Request) ProtoMessage() {} +func (*GenerateQrcodeRequest) ProtoMessage() {} -func (x *Request) ProtoReflect() protoreflect.Message { +func (x *GenerateQrcodeRequest) ProtoReflect() protoreflect.Message { mi := &file_analysis_analysis_proto_msgTypes[0] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) @@ -53,21 +53,16 @@ func (x *Request) ProtoReflect() protoreflect.Message { return mi.MessageOf(x) } -// Deprecated: Use Request.ProtoReflect.Descriptor instead. -func (*Request) Descriptor() ([]byte, []int) { +// Deprecated: Use GenerateQrcodeRequest.ProtoReflect.Descriptor instead. +func (*GenerateQrcodeRequest) Descriptor() ([]byte, []int) { return file_analysis_analysis_proto_rawDescGZIP(), []int{0} } -func (x *Request) GetPing() string { - if x != nil { - return x.Ping - } - return "" -} - type Response struct { state protoimpl.MessageState `protogen:"open.v1"` - Pong string `protobuf:"bytes,1,opt,name=pong,proto3" json:"pong,omitempty"` + Code int32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + Msg string `protobuf:"bytes,2,opt,name=msg,proto3" json:"msg,omitempty"` + Data string `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -102,9 +97,23 @@ func (*Response) Descriptor() ([]byte, []int) { return file_analysis_analysis_proto_rawDescGZIP(), []int{1} } -func (x *Response) GetPong() string { +func (x *Response) GetCode() int32 { if x != nil { - return x.Pong + return x.Code + } + return 0 +} + +func (x *Response) GetMsg() string { + if x != nil { + return x.Msg + } + return "" +} + +func (x *Response) GetData() string { + if x != nil { + return x.Data } return "" } @@ -113,13 +122,14 @@ var File_analysis_analysis_proto protoreflect.FileDescriptor const file_analysis_analysis_proto_rawDesc = "" + "\n" + - "\x17analysis/analysis.proto\x12\banalysis\"\x1d\n" + - "\aRequest\x12\x12\n" + - "\x04ping\x18\x01 \x01(\tR\x04ping\"\x1e\n" + + "\x17analysis/analysis.proto\x12\banalysis\x1a\x1cgoogle/api/annotations.proto\"\x17\n" + + "\x15GenerateQrcodeRequest\"D\n" + "\bResponse\x12\x12\n" + - "\x04pong\x18\x01 \x01(\tR\x04pong29\n" + - "\bAnalysis\x12-\n" + - "\x04Ping\x12\x11.analysis.Request\x1a\x12.analysis.ResponseB\x1cZ\x1alone-services/rpc/analysisb\x06proto3" + "\x04code\x18\x01 \x01(\x05R\x04code\x12\x10\n" + + "\x03msg\x18\x02 \x01(\tR\x03msg\x12\x12\n" + + "\x04data\x18\x03 \x01(\tR\x04data2\x82\x01\n" + + "\fAnalysisAuth\x12r\n" + + "\x0eGenerateQrcode\x12\x1f.analysis.GenerateQrcodeRequest\x1a\x12.analysis.Response\"+\x82\xd3\xe4\x93\x02%:\x01*\" /api/v2/analysis/qrcode/generateB\x1cZ\x1alone-services/rpc/analysisb\x06proto3" var ( file_analysis_analysis_proto_rawDescOnce sync.Once @@ -135,12 +145,12 @@ func file_analysis_analysis_proto_rawDescGZIP() []byte { var file_analysis_analysis_proto_msgTypes = make([]protoimpl.MessageInfo, 2) var file_analysis_analysis_proto_goTypes = []any{ - (*Request)(nil), // 0: analysis.Request - (*Response)(nil), // 1: analysis.Response + (*GenerateQrcodeRequest)(nil), // 0: analysis.GenerateQrcodeRequest + (*Response)(nil), // 1: analysis.Response } var file_analysis_analysis_proto_depIdxs = []int32{ - 0, // 0: analysis.Analysis.Ping:input_type -> analysis.Request - 1, // 1: analysis.Analysis.Ping:output_type -> analysis.Response + 0, // 0: analysis.AnalysisAuth.GenerateQrcode:input_type -> analysis.GenerateQrcodeRequest + 1, // 1: analysis.AnalysisAuth.GenerateQrcode:output_type -> analysis.Response 1, // [1:2] is the sub-list for method output_type 0, // [0:1] is the sub-list for method input_type 0, // [0:0] is the sub-list for extension type_name diff --git a/rpc/analysis/pb/analysis_grpc.pb.go b/rpc/analysis/pb/analysis_grpc.pb.go index 187b7c4..760a40c 100644 --- a/rpc/analysis/pb/analysis_grpc.pb.go +++ b/rpc/analysis/pb/analysis_grpc.pb.go @@ -19,101 +19,101 @@ import ( const _ = grpc.SupportPackageIsVersion9 const ( - Analysis_Ping_FullMethodName = "/analysis.Analysis/Ping" + AnalysisAuth_GenerateQrcode_FullMethodName = "/analysis.AnalysisAuth/GenerateQrcode" ) -// AnalysisClient is the client API for Analysis service. +// AnalysisAuthClient is the client API for AnalysisAuth service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. -type AnalysisClient interface { - Ping(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) +type AnalysisAuthClient interface { + GenerateQrcode(ctx context.Context, in *GenerateQrcodeRequest, opts ...grpc.CallOption) (*Response, error) } -type analysisClient struct { +type analysisAuthClient struct { cc grpc.ClientConnInterface } -func NewAnalysisClient(cc grpc.ClientConnInterface) AnalysisClient { - return &analysisClient{cc} +func NewAnalysisAuthClient(cc grpc.ClientConnInterface) AnalysisAuthClient { + return &analysisAuthClient{cc} } -func (c *analysisClient) Ping(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) { +func (c *analysisAuthClient) GenerateQrcode(ctx context.Context, in *GenerateQrcodeRequest, opts ...grpc.CallOption) (*Response, error) { cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) out := new(Response) - err := c.cc.Invoke(ctx, Analysis_Ping_FullMethodName, in, out, cOpts...) + err := c.cc.Invoke(ctx, AnalysisAuth_GenerateQrcode_FullMethodName, in, out, cOpts...) if err != nil { return nil, err } return out, nil } -// AnalysisServer is the server API for Analysis service. -// All implementations must embed UnimplementedAnalysisServer +// AnalysisAuthServer is the server API for AnalysisAuth service. +// All implementations must embed UnimplementedAnalysisAuthServer // for forward compatibility. -type AnalysisServer interface { - Ping(context.Context, *Request) (*Response, error) - mustEmbedUnimplementedAnalysisServer() +type AnalysisAuthServer interface { + GenerateQrcode(context.Context, *GenerateQrcodeRequest) (*Response, error) + mustEmbedUnimplementedAnalysisAuthServer() } -// UnimplementedAnalysisServer must be embedded to have +// UnimplementedAnalysisAuthServer 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 UnimplementedAnalysisServer struct{} +type UnimplementedAnalysisAuthServer struct{} -func (UnimplementedAnalysisServer) Ping(context.Context, *Request) (*Response, error) { - return nil, status.Error(codes.Unimplemented, "method Ping not implemented") +func (UnimplementedAnalysisAuthServer) GenerateQrcode(context.Context, *GenerateQrcodeRequest) (*Response, error) { + return nil, status.Error(codes.Unimplemented, "method GenerateQrcode not implemented") } -func (UnimplementedAnalysisServer) mustEmbedUnimplementedAnalysisServer() {} -func (UnimplementedAnalysisServer) testEmbeddedByValue() {} +func (UnimplementedAnalysisAuthServer) mustEmbedUnimplementedAnalysisAuthServer() {} +func (UnimplementedAnalysisAuthServer) testEmbeddedByValue() {} -// UnsafeAnalysisServer may be embedded to opt out of forward compatibility for this service. -// Use of this interface is not recommended, as added methods to AnalysisServer will +// UnsafeAnalysisAuthServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to AnalysisAuthServer will // result in compilation errors. -type UnsafeAnalysisServer interface { - mustEmbedUnimplementedAnalysisServer() +type UnsafeAnalysisAuthServer interface { + mustEmbedUnimplementedAnalysisAuthServer() } -func RegisterAnalysisServer(s grpc.ServiceRegistrar, srv AnalysisServer) { - // If the following call panics, it indicates UnimplementedAnalysisServer was +func RegisterAnalysisAuthServer(s grpc.ServiceRegistrar, srv AnalysisAuthServer) { + // If the following call panics, it indicates UnimplementedAnalysisAuthServer 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(&Analysis_ServiceDesc, srv) + s.RegisterService(&AnalysisAuth_ServiceDesc, srv) } -func _Analysis_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { - in := new(Request) +func _AnalysisAuth_GenerateQrcode_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GenerateQrcodeRequest) if err := dec(in); err != nil { return nil, err } if interceptor == nil { - return srv.(AnalysisServer).Ping(ctx, in) + return srv.(AnalysisAuthServer).GenerateQrcode(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: Analysis_Ping_FullMethodName, + FullMethod: AnalysisAuth_GenerateQrcode_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { - return srv.(AnalysisServer).Ping(ctx, req.(*Request)) + return srv.(AnalysisAuthServer).GenerateQrcode(ctx, req.(*GenerateQrcodeRequest)) } return interceptor(ctx, in, info, handler) } -// Analysis_ServiceDesc is the grpc.ServiceDesc for Analysis service. +// AnalysisAuth_ServiceDesc is the grpc.ServiceDesc for AnalysisAuth service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) -var Analysis_ServiceDesc = grpc.ServiceDesc{ - ServiceName: "analysis.Analysis", - HandlerType: (*AnalysisServer)(nil), +var AnalysisAuth_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "analysis.AnalysisAuth", + HandlerType: (*AnalysisAuthServer)(nil), Methods: []grpc.MethodDesc{ { - MethodName: "Ping", - Handler: _Analysis_Ping_Handler, + MethodName: "GenerateQrcode", + Handler: _AnalysisAuth_GenerateQrcode_Handler, }, }, Streams: []grpc.StreamDesc{}, diff --git a/services/analysis/analysis.go b/services/analysis/analysis.go index 3f86326..8b73326 100644 --- a/services/analysis/analysis.go +++ b/services/analysis/analysis.go @@ -130,7 +130,7 @@ func main() { ctx := svc.NewServiceContext(c, db) s := zrpc.MustNewServer(rpcConf, func(grpcServer *grpc.Server) { - analysis.RegisterAnalysisServer(grpcServer, server.NewAnalysisServer(ctx)) + analysis.RegisterAnalysisAuthServer(grpcServer, server.NewAnalysisAuthServer(ctx)) if mode == service.DevMode || mode == service.TestMode { reflection.Register(grpcServer) } diff --git a/services/analysis/analysisauth/analysisAuth.go b/services/analysis/analysisauth/analysisAuth.go new file mode 100644 index 0000000..255d0f7 --- /dev/null +++ b/services/analysis/analysisauth/analysisAuth.go @@ -0,0 +1,38 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.1 +// Source: analysis.proto + +package analysisauth + +import ( + "context" + + "lone-services/rpc/analysis/pb" + + "github.com/zeromicro/go-zero/zrpc" + "google.golang.org/grpc" +) + +type ( + GenerateQrcodeRequest = analysis.GenerateQrcodeRequest + Response = analysis.Response + + AnalysisAuth interface { + GenerateQrcode(ctx context.Context, in *GenerateQrcodeRequest, opts ...grpc.CallOption) (*Response, error) + } + + defaultAnalysisAuth struct { + cli zrpc.Client + } +) + +func NewAnalysisAuth(cli zrpc.Client) AnalysisAuth { + return &defaultAnalysisAuth{ + cli: cli, + } +} + +func (m *defaultAnalysisAuth) GenerateQrcode(ctx context.Context, in *GenerateQrcodeRequest, opts ...grpc.CallOption) (*Response, error) { + client := analysis.NewAnalysisAuthClient(m.cli.Conn()) + return client.GenerateQrcode(ctx, in, opts...) +} diff --git a/services/analysis/analysisclient/analysis.go b/services/analysis/analysisclient/analysis.go deleted file mode 100644 index a31f142..0000000 --- a/services/analysis/analysisclient/analysis.go +++ /dev/null @@ -1,38 +0,0 @@ -// Code generated by goctl. DO NOT EDIT. -// goctl 1.10.1 -// Source: analysis.proto - -package analysisClient - -import ( - "context" - - "lone-services/rpc/analysis/pb" - - "github.com/zeromicro/go-zero/zrpc" - "google.golang.org/grpc" -) - -type ( - Request = analysis.Request - Response = analysis.Response - - Analysis interface { - Ping(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) - } - - defaultAnalysis struct { - cli zrpc.Client - } -) - -func NewAnalysis(cli zrpc.Client) Analysis { - return &defaultAnalysis{ - cli: cli, - } -} - -func (m *defaultAnalysis) Ping(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error) { - client := analysis.NewAnalysisClient(m.cli.Conn()) - return client.Ping(ctx, in, opts...) -} diff --git a/services/analysis/internal/config/wechat.go b/services/analysis/internal/config/wechat.go new file mode 100644 index 0000000..1ca72d3 --- /dev/null +++ b/services/analysis/internal/config/wechat.go @@ -0,0 +1,45 @@ +package config + +import ( + "fmt" + + "lone-services/pkg/utils" + "lone-services/pkg/wechat/qrcode" +) + +type WechatQrcodeConfig struct { + AppID string + Secret string + Page string + Width int + EnvVersion string +} + +func LoadWechatQrcodeConfig() (*WechatQrcodeConfig, error) { + + appID := utils.GetConfigString("wechat.appid") + secret := utils.GetConfigString("wechat.secret") + if appID == utils.StringEmpty || secret == utils.StringEmpty { + return nil, fmt.Errorf("wechat config missing") + } + + page := utils.GetConfigString("wechat.page") + + width := utils.GetConfigInt("wechat.width") + envVersion := utils.GetConfigString("wechat.env_version") + if envVersion == utils.StringEmpty { + envVersion = "trial" + } + + return &WechatQrcodeConfig{ + AppID: appID, + Secret: secret, + Page: page, + Width: width, + EnvVersion: envVersion, + }, nil +} + +func (c *WechatQrcodeConfig) NewQrcodeClient() *qrcode.Client { + return qrcode.NewClient(c.AppID, c.Secret) +} diff --git a/services/analysis/internal/dao/qrcode.go b/services/analysis/internal/dao/qrcode.go new file mode 100644 index 0000000..5aced9c --- /dev/null +++ b/services/analysis/internal/dao/qrcode.go @@ -0,0 +1,17 @@ +package dao + +import "time" + +const ( + RedisKeyQRSession = "qr:session:" + + QrcodeSessionPending = 1 // 待扫码 + QrcodeSessionScanned = 2 // 已扫码未登录 + QrcodeSessionLogined = 3 // 已扫码已登录 + + QrcodeSessionTTL = 5 * time.Minute +) + +type QrcodeSessionStatus struct { + Status int `json:"status"` +} diff --git a/services/analysis/internal/logic/generateQrcodeLogic.go b/services/analysis/internal/logic/generateQrcodeLogic.go new file mode 100644 index 0000000..588fc1a --- /dev/null +++ b/services/analysis/internal/logic/generateQrcodeLogic.go @@ -0,0 +1,114 @@ +package logic + +import ( + "context" + "fmt" + + "lone-services/pkg/redis" + "lone-services/pkg/utils" + "lone-services/pkg/wechat/qrcode" + analysis "lone-services/rpc/analysis/pb" + chore "lone-services/rpc/chore/pb" + "lone-services/services/analysis/internal/config" + "lone-services/services/analysis/internal/dao" + "lone-services/services/analysis/internal/svc" + + "github.com/google/uuid" + jsoniter "github.com/json-iterator/go" + "github.com/zeromicro/go-zero/core/logx" +) + +type GenerateQrcodeLogic struct { + ctx context.Context + svcCtx *svc.ServiceContext + logx.Logger +} + +func NewGenerateQrcodeLogic(ctx context.Context, svcCtx *svc.ServiceContext) *GenerateQrcodeLogic { + return &GenerateQrcodeLogic{ + ctx: ctx, + svcCtx: svcCtx, + Logger: logx.WithContext(ctx), + } +} + +func (l *GenerateQrcodeLogic) GenerateQrcode(in *analysis.GenerateQrcodeRequest) (*analysis.Response, error) { + sessionID, err := l.createSession() + if err != nil { + l.Errorf("generate qrcode session: %v", err) + return outResponse(utils.Fail, "create qrcode session failed"), nil + } + + wxCfg, err := config.LoadWechatQrcodeConfig() + if err != nil { + l.Errorf("generate qrcode wechat config: %v", err) + return outResponse(utils.Fail, "wechat config missing"), nil + } + + img, err := wxCfg.NewQrcodeClient().CreateWxaCode(l.ctx, &qrcode.WxaCodeRequest{ + Path: qrcode.BuildPath(wxCfg.Page, map[string]string{ + "session_id": sessionID, + }), + Width: wxCfg.Width, + EnvVersion: wxCfg.EnvVersion, + AutoColor: false, + LineColor: &qrcode.LineColor{R: 255, G: 255, B: 255}, + IsHyaline: true, + }) + if err != nil { + l.Errorf("generate qrcode wechat: %v", err) + return outResponse(utils.Fail, err.Error()), nil + } + + qrURL, err := l.uploadQrcode(img) + if err != nil { + l.Errorf("generate qrcode upload: %v", err) + return outResponse(utils.Fail, "upload qrcode failed"), nil + } + + return okResponse(map[string]string{ + "session_id": sessionID, + "qrcode": qrURL, + }), nil +} + +func (l *GenerateQrcodeLogic) createSession() (string, error) { + if redis.Client == nil { + return "", fmt.Errorf("redis not initialized") + } + + sessionID := uuid.New().String() + payload, err := jsoniter.MarshalToString(dao.QrcodeSessionStatus{Status: dao.QrcodeSessionPending}) + if err != nil { + return "", err + } + + key := dao.RedisKeyQRSession + sessionID + if err := redis.Client.Set(l.ctx, key, payload, dao.QrcodeSessionTTL).Err(); err != nil { + return "", err + } + return sessionID, nil +} + +func (l *GenerateQrcodeLogic) uploadQrcode(content []byte) (string, error) { + if l.svcCtx.ChoreSvcName == utils.StringEmpty { + return "", fmt.Errorf("chore service name empty") + } + + cli, err := svc.GetRpcClient(l.svcCtx.ChoreSvcName) + if err != nil { + return "", err + } + + res, err := chore.NewChoreClient(cli.Conn()).InternalUpload(l.ctx, &chore.InternalUploadReq{ + Filename: uuid.New().String() + ".png", + Content: content, + }) + if err != nil { + return "", err + } + if res == nil || res.Url == utils.StringEmpty { + return "", fmt.Errorf("chore upload empty url") + } + return res.Url, nil +} diff --git a/services/analysis/internal/logic/pinglogic.go b/services/analysis/internal/logic/pinglogic.go deleted file mode 100644 index d6abd79..0000000 --- a/services/analysis/internal/logic/pinglogic.go +++ /dev/null @@ -1,30 +0,0 @@ -package logic - -import ( - "context" - - "lone-services/services/analysis/analysis" - "lone-services/services/analysis/internal/svc" - - "github.com/zeromicro/go-zero/core/logx" -) - -type PingLogic struct { - ctx context.Context - svcCtx *svc.ServiceContext - logx.Logger -} - -func NewPingLogic(ctx context.Context, svcCtx *svc.ServiceContext) *PingLogic { - return &PingLogic{ - ctx: ctx, - svcCtx: svcCtx, - Logger: logx.WithContext(ctx), - } -} - -func (l *PingLogic) Ping(in *analysis.Request) (*analysis.Response, error) { - // todo: add your logic here and delete this line - - return &analysis.Response{}, nil -} diff --git a/services/analysis/internal/logic/response.go b/services/analysis/internal/logic/response.go new file mode 100644 index 0000000..8fa2c0e --- /dev/null +++ b/services/analysis/internal/logic/response.go @@ -0,0 +1,31 @@ +package logic + +import ( + "lone-services/pkg/utils" + analysis "lone-services/rpc/analysis/pb" + + jsoniter "github.com/json-iterator/go" +) + +func okResponse(data any) *analysis.Response { + buf, _ := jsoniter.Marshal(data) + return &analysis.Response{ + Code: utils.Ok.Code, + Msg: utils.Ok.Msg, + Data: string(buf), + } +} + +func failResponse(status utils.Status) *analysis.Response { + return &analysis.Response{ + Code: status.Code, + Msg: status.Msg, + } +} + +func outResponse(status utils.Status, msg string) *analysis.Response { + return &analysis.Response{ + Code: status.Code, + Msg: msg, + } +} diff --git a/services/analysis/internal/server/analysisAuthServer.go b/services/analysis/internal/server/analysisAuthServer.go new file mode 100644 index 0000000..05dd613 --- /dev/null +++ b/services/analysis/internal/server/analysisAuthServer.go @@ -0,0 +1,29 @@ +// Code generated by goctl. DO NOT EDIT. +// goctl 1.10.1 +// Source: analysis.proto + +package server + +import ( + "context" + + "lone-services/rpc/analysis/pb" + "lone-services/services/analysis/internal/logic" + "lone-services/services/analysis/internal/svc" +) + +type AnalysisAuthServer struct { + svcCtx *svc.ServiceContext + analysis.UnimplementedAnalysisAuthServer +} + +func NewAnalysisAuthServer(svcCtx *svc.ServiceContext) *AnalysisAuthServer { + return &AnalysisAuthServer{ + svcCtx: svcCtx, + } +} + +func (s *AnalysisAuthServer) GenerateQrcode(ctx context.Context, in *analysis.GenerateQrcodeRequest) (*analysis.Response, error) { + l := logic.NewGenerateQrcodeLogic(ctx, s.svcCtx) + return l.GenerateQrcode(in) +} diff --git a/services/analysis/internal/server/analysisServer.go b/services/analysis/internal/server/analysisServer.go deleted file mode 100644 index a2ef685..0000000 --- a/services/analysis/internal/server/analysisServer.go +++ /dev/null @@ -1,29 +0,0 @@ -// Code generated by goctl. DO NOT EDIT. -// goctl 1.10.1 -// Source: analysis.proto - -package server - -import ( - "context" - - "lone-services/rpc/analysis/pb" - "lone-services/services/analysis/internal/logic" - "lone-services/services/analysis/internal/svc" -) - -type AnalysisServer struct { - svcCtx *svc.ServiceContext - analysis.UnimplementedAnalysisServer -} - -func NewAnalysisServer(svcCtx *svc.ServiceContext) *AnalysisServer { - return &AnalysisServer{ - svcCtx: svcCtx, - } -} - -func (s *AnalysisServer) Ping(ctx context.Context, in *analysis.Request) (*analysis.Response, error) { - l := logic.NewPingLogic(ctx, s.svcCtx) - return l.Ping(in) -} diff --git a/services/analysis/run.toml b/services/analysis/run.toml new file mode 100644 index 0000000..090d0de --- /dev/null +++ b/services/analysis/run.toml @@ -0,0 +1,61 @@ +[base] + login_out_time=43200 + login_refresh_out_time=83200 + name = "analysis-service" + listenOn = "0.0.0.0:11800" + mode = "dev" + +[log] + path = "logs" + serviceName = "analysis-service" + mode = "file" + encoding = "plain" + level = "info" + keepDays = 7 + maxSize = 50 + maxBackups = 5 + compress = false + +[mysql] + host = '39.106.171.204' + port = 33066 + user = 'root' + password = 'MOLXRZNOU4Y4' + database = 'dms-analysis' + charset = 'utf8mb4' + prefix = '' + debug = true + +[mysql_read] + host = '39.106.171.204' + port = 33066 + user = 'root' + password = 'MOLXRZNOU4Y4' + database = 'dms-analysis' + charset = 'utf8mb4' + prefix = '' + +[redis] + host = '39.106.171.204' + password = 'lLMLcuPpzSj' + port = 6379 + db = 0 + +[log-redis] + host = '39.106.171.204' + password = 'lLMLcuPpzSj' + port = 6379 + db = 3 + +[wechat] + appid = 'wxa6f5d68dcbc955c7' + secret = '657bc95ab1eb4462913c056802b677c1' + login_page = 'pagesB/userLogin/index' + width = 430 + env_version = 'trial' + +[services] + chore = "chore-service" + +[encrypt] + data_key = "u2t9T3luZtoRfhBstkFN6TiIMW38BA8a" \ No newline at end of file diff --git a/services/chore/internal/logic/internalUploadLogic.go b/services/chore/internal/logic/internalUploadLogic.go index de35493..75caddc 100644 --- a/services/chore/internal/logic/internalUploadLogic.go +++ b/services/chore/internal/logic/internalUploadLogic.go @@ -30,7 +30,6 @@ func NewInternalUploadLogic(ctx context.Context, svcCtx *svc.ServiceContext) *In } } -// InternalUpload 对内:服务间直传 OSS(无 HTTP 暴露) func (l *InternalUploadLogic) InternalUpload(in *chore.InternalUploadReq) (*chore.InternalUploadData, error) { var req validator.UploadValidator if msg := validate.ValidateFromProto(in, &req); msg != utils.StringEmpty {