Initial commit
This commit is contained in:
commit
5be5435bb0
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
.DS_Store
|
||||||
645
api/proto/gateway.pb.go
Normal file
645
api/proto/gateway.pb.go
Normal file
@ -0,0 +1,645 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.34.2
|
||||||
|
// protoc v5.29.2
|
||||||
|
// source: gateway.proto
|
||||||
|
|
||||||
|
package gatewaypb
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
type LoginRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
LoginType string `protobuf:"bytes,1,opt,name=login_type,json=loginType,proto3" json:"login_type,omitempty"`
|
||||||
|
Account string `protobuf:"bytes,2,opt,name=account,proto3" json:"account,omitempty"`
|
||||||
|
Password string `protobuf:"bytes,3,opt,name=password,proto3" json:"password,omitempty"`
|
||||||
|
CountryCode string `protobuf:"bytes,4,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"`
|
||||||
|
VerifyCode string `protobuf:"bytes,5,opt,name=verify_code,json=verifyCode,proto3" json:"verify_code,omitempty"`
|
||||||
|
Provider string `protobuf:"bytes,6,opt,name=provider,proto3" json:"provider,omitempty"`
|
||||||
|
ProviderToken string `protobuf:"bytes,7,opt,name=provider_token,json=providerToken,proto3" json:"provider_token,omitempty"`
|
||||||
|
DeviceId string `protobuf:"bytes,8,opt,name=device_id,json=deviceId,proto3" json:"device_id,omitempty"`
|
||||||
|
Platform string `protobuf:"bytes,9,opt,name=platform,proto3" json:"platform,omitempty"`
|
||||||
|
AppVersion string `protobuf:"bytes,10,opt,name=app_version,json=appVersion,proto3" json:"app_version,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) Reset() {
|
||||||
|
*x = LoginRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_gateway_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*LoginRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *LoginRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_gateway_proto_msgTypes[0]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use LoginRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*LoginRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_gateway_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) GetLoginType() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.LoginType
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) GetAccount() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Account
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) GetPassword() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Password
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) GetCountryCode() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.CountryCode
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) GetVerifyCode() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.VerifyCode
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) GetProvider() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Provider
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) GetProviderToken() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ProviderToken
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) GetDeviceId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.DeviceId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) GetPlatform() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Platform
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginRequest) GetAppVersion() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AppVersion
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserProfile struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||||
|
Nickname string `protobuf:"bytes,2,opt,name=nickname,proto3" json:"nickname,omitempty"`
|
||||||
|
AvatarUrl string `protobuf:"bytes,3,opt,name=avatar_url,json=avatarUrl,proto3" json:"avatar_url,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UserProfile) Reset() {
|
||||||
|
*x = UserProfile{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_gateway_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UserProfile) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*UserProfile) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *UserProfile) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_gateway_proto_msgTypes[1]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use UserProfile.ProtoReflect.Descriptor instead.
|
||||||
|
func (*UserProfile) Descriptor() ([]byte, []int) {
|
||||||
|
return file_gateway_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UserProfile) GetUserId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.UserId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UserProfile) GetNickname() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Nickname
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *UserProfile) GetAvatarUrl() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AvatarUrl
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type LoginResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
UserId string `protobuf:"bytes,1,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||||
|
AccessToken string `protobuf:"bytes,2,opt,name=access_token,json=accessToken,proto3" json:"access_token,omitempty"`
|
||||||
|
RefreshToken string `protobuf:"bytes,3,opt,name=refresh_token,json=refreshToken,proto3" json:"refresh_token,omitempty"`
|
||||||
|
ExpiresIn int64 `protobuf:"varint,4,opt,name=expires_in,json=expiresIn,proto3" json:"expires_in,omitempty"`
|
||||||
|
IsNewUser bool `protobuf:"varint,5,opt,name=is_new_user,json=isNewUser,proto3" json:"is_new_user,omitempty"`
|
||||||
|
Profile *UserProfile `protobuf:"bytes,6,opt,name=profile,proto3" json:"profile,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginResponse) Reset() {
|
||||||
|
*x = LoginResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_gateway_proto_msgTypes[2]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*LoginResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *LoginResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_gateway_proto_msgTypes[2]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use LoginResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*LoginResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_gateway_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginResponse) GetUserId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.UserId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginResponse) GetAccessToken() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.AccessToken
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginResponse) GetRefreshToken() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.RefreshToken
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginResponse) GetExpiresIn() int64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.ExpiresIn
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginResponse) GetIsNewUser() bool {
|
||||||
|
if x != nil {
|
||||||
|
return x.IsNewUser
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *LoginResponse) GetProfile() *UserProfile {
|
||||||
|
if x != nil {
|
||||||
|
return x.Profile
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryOrderRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
OrderNo string `protobuf:"bytes,1,opt,name=order_no,json=orderNo,proto3" json:"order_no,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *QueryOrderRequest) Reset() {
|
||||||
|
*x = QueryOrderRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_gateway_proto_msgTypes[3]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *QueryOrderRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*QueryOrderRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *QueryOrderRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_gateway_proto_msgTypes[3]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use QueryOrderRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*QueryOrderRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_gateway_proto_rawDescGZIP(), []int{3}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *QueryOrderRequest) GetOrderNo() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.OrderNo
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryOrderResponse struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
OrderNo string `protobuf:"bytes,1,opt,name=order_no,json=orderNo,proto3" json:"order_no,omitempty"`
|
||||||
|
UserId string `protobuf:"bytes,2,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"`
|
||||||
|
Status string `protobuf:"bytes,3,opt,name=status,proto3" json:"status,omitempty"`
|
||||||
|
Amount string `protobuf:"bytes,4,opt,name=amount,proto3" json:"amount,omitempty"`
|
||||||
|
Currency string `protobuf:"bytes,5,opt,name=currency,proto3" json:"currency,omitempty"`
|
||||||
|
Subject string `protobuf:"bytes,6,opt,name=subject,proto3" json:"subject,omitempty"`
|
||||||
|
PayMethod string `protobuf:"bytes,7,opt,name=pay_method,json=payMethod,proto3" json:"pay_method,omitempty"`
|
||||||
|
PaidAt string `protobuf:"bytes,8,opt,name=paid_at,json=paidAt,proto3" json:"paid_at,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *QueryOrderResponse) Reset() {
|
||||||
|
*x = QueryOrderResponse{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_gateway_proto_msgTypes[4]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *QueryOrderResponse) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*QueryOrderResponse) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *QueryOrderResponse) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_gateway_proto_msgTypes[4]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use QueryOrderResponse.ProtoReflect.Descriptor instead.
|
||||||
|
func (*QueryOrderResponse) Descriptor() ([]byte, []int) {
|
||||||
|
return file_gateway_proto_rawDescGZIP(), []int{4}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *QueryOrderResponse) GetOrderNo() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.OrderNo
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *QueryOrderResponse) GetUserId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.UserId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *QueryOrderResponse) GetStatus() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Status
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *QueryOrderResponse) GetAmount() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Amount
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *QueryOrderResponse) GetCurrency() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Currency
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *QueryOrderResponse) GetSubject() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Subject
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *QueryOrderResponse) GetPayMethod() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.PayMethod
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *QueryOrderResponse) GetPaidAt() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.PaidAt
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_gateway_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
var file_gateway_proto_rawDesc = []byte{
|
||||||
|
0x0a, 0x0d, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12,
|
||||||
|
0x19, 0x63, 0x68, 0x61, 0x74, 0x61, 0x70, 0x70, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e,
|
||||||
|
0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x22, 0xc4, 0x02, 0x0a, 0x0c, 0x4c,
|
||||||
|
0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6c,
|
||||||
|
0x6f, 0x67, 0x69, 0x6e, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
|
0x09, 0x6c, 0x6f, 0x67, 0x69, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63,
|
||||||
|
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x63,
|
||||||
|
0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
|
||||||
|
0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x61, 0x73, 0x73, 0x77, 0x6f, 0x72, 0x64,
|
||||||
|
0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x5f, 0x63, 0x6f, 0x64, 0x65,
|
||||||
|
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x72, 0x79, 0x43,
|
||||||
|
0x6f, 0x64, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x5f, 0x63, 0x6f,
|
||||||
|
0x64, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x76, 0x65, 0x72, 0x69, 0x66, 0x79,
|
||||||
|
0x43, 0x6f, 0x64, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72,
|
||||||
|
0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72,
|
||||||
|
0x12, 0x25, 0x0a, 0x0e, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x5f, 0x74, 0x6f, 0x6b,
|
||||||
|
0x65, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64,
|
||||||
|
0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x64, 0x65, 0x76, 0x69, 0x63,
|
||||||
|
0x65, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x64, 0x65, 0x76, 0x69,
|
||||||
|
0x63, 0x65, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d,
|
||||||
|
0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x70, 0x6c, 0x61, 0x74, 0x66, 0x6f, 0x72, 0x6d,
|
||||||
|
0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x70, 0x70, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18,
|
||||||
|
0x0a, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x61, 0x70, 0x70, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f,
|
||||||
|
0x6e, 0x22, 0x61, 0x0a, 0x0b, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65,
|
||||||
|
0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
|
0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x6e, 0x69, 0x63,
|
||||||
|
0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x6e, 0x69, 0x63,
|
||||||
|
0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x61, 0x76, 0x61, 0x74, 0x61, 0x72, 0x5f,
|
||||||
|
0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x61, 0x76, 0x61, 0x74, 0x61,
|
||||||
|
0x72, 0x55, 0x72, 0x6c, 0x22, 0xf1, 0x01, 0x0a, 0x0d, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65,
|
||||||
|
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69,
|
||||||
|
0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12,
|
||||||
|
0x21, 0x0a, 0x0c, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18,
|
||||||
|
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x61, 0x63, 0x63, 0x65, 0x73, 0x73, 0x54, 0x6f, 0x6b,
|
||||||
|
0x65, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x66, 0x72, 0x65, 0x73, 0x68, 0x5f, 0x74, 0x6f,
|
||||||
|
0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x66, 0x72, 0x65,
|
||||||
|
0x73, 0x68, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x78, 0x70, 0x69, 0x72,
|
||||||
|
0x65, 0x73, 0x5f, 0x69, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x65, 0x78, 0x70,
|
||||||
|
0x69, 0x72, 0x65, 0x73, 0x49, 0x6e, 0x12, 0x1e, 0x0a, 0x0b, 0x69, 0x73, 0x5f, 0x6e, 0x65, 0x77,
|
||||||
|
0x5f, 0x75, 0x73, 0x65, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x4e,
|
||||||
|
0x65, 0x77, 0x55, 0x73, 0x65, 0x72, 0x12, 0x40, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c,
|
||||||
|
0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x63, 0x68, 0x61, 0x74, 0x61, 0x70,
|
||||||
|
0x70, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79,
|
||||||
|
0x2e, 0x76, 0x31, 0x2e, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52,
|
||||||
|
0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x22, 0x2e, 0x0a, 0x11, 0x51, 0x75, 0x65, 0x72,
|
||||||
|
0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a,
|
||||||
|
0x08, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x5f, 0x6e, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
|
0x07, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x4e, 0x6f, 0x22, 0xe6, 0x01, 0x0a, 0x12, 0x51, 0x75, 0x65,
|
||||||
|
0x72, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||||
|
0x19, 0x0a, 0x08, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x5f, 0x6e, 0x6f, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||||
|
0x09, 0x52, 0x07, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x4e, 0x6f, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73,
|
||||||
|
0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65,
|
||||||
|
0x72, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x03, 0x20,
|
||||||
|
0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x16, 0x0a, 0x06, 0x61,
|
||||||
|
0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x61, 0x6d, 0x6f,
|
||||||
|
0x75, 0x6e, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18,
|
||||||
|
0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x12,
|
||||||
|
0x18, 0x0a, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09,
|
||||||
|
0x52, 0x07, 0x73, 0x75, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x61, 0x79,
|
||||||
|
0x5f, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70,
|
||||||
|
0x61, 0x79, 0x4d, 0x65, 0x74, 0x68, 0x6f, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x61, 0x69, 0x64,
|
||||||
|
0x5f, 0x61, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x61, 0x69, 0x64, 0x41,
|
||||||
|
0x74, 0x32, 0x69, 0x0a, 0x0b, 0x43, 0x68, 0x61, 0x74, 0x41, 0x70, 0x70, 0x55, 0x73, 0x65, 0x72,
|
||||||
|
0x12, 0x5a, 0x0a, 0x05, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x12, 0x27, 0x2e, 0x63, 0x68, 0x61, 0x74,
|
||||||
|
0x61, 0x70, 0x70, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77,
|
||||||
|
0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65,
|
||||||
|
0x73, 0x74, 0x1a, 0x28, 0x2e, 0x63, 0x68, 0x61, 0x74, 0x61, 0x70, 0x70, 0x67, 0x61, 0x74, 0x65,
|
||||||
|
0x77, 0x61, 0x79, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x76, 0x31, 0x2e, 0x4c,
|
||||||
|
0x6f, 0x67, 0x69, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x77, 0x0a, 0x0a,
|
||||||
|
0x43, 0x68, 0x61, 0x74, 0x41, 0x70, 0x70, 0x50, 0x61, 0x79, 0x12, 0x69, 0x0a, 0x0a, 0x51, 0x75,
|
||||||
|
0x65, 0x72, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x12, 0x2c, 0x2e, 0x63, 0x68, 0x61, 0x74, 0x61,
|
||||||
|
0x70, 0x70, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61,
|
||||||
|
0x79, 0x2e, 0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52,
|
||||||
|
0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x63, 0x68, 0x61, 0x74, 0x61, 0x70, 0x70,
|
||||||
|
0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2e,
|
||||||
|
0x76, 0x31, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x4f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x73,
|
||||||
|
0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x24, 0x5a, 0x22, 0x63, 0x68, 0x61, 0x74, 0x61, 0x70, 0x70,
|
||||||
|
0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74,
|
||||||
|
0x6f, 0x3b, 0x67, 0x61, 0x74, 0x65, 0x77, 0x61, 0x79, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f,
|
||||||
|
0x74, 0x6f, 0x33,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_gateway_proto_rawDescOnce sync.Once
|
||||||
|
file_gateway_proto_rawDescData = file_gateway_proto_rawDesc
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_gateway_proto_rawDescGZIP() []byte {
|
||||||
|
file_gateway_proto_rawDescOnce.Do(func() {
|
||||||
|
file_gateway_proto_rawDescData = protoimpl.X.CompressGZIP(file_gateway_proto_rawDescData)
|
||||||
|
})
|
||||||
|
return file_gateway_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_gateway_proto_msgTypes = make([]protoimpl.MessageInfo, 5)
|
||||||
|
var file_gateway_proto_goTypes = []any{
|
||||||
|
(*LoginRequest)(nil), // 0: chatappgateway.gateway.v1.LoginRequest
|
||||||
|
(*UserProfile)(nil), // 1: chatappgateway.gateway.v1.UserProfile
|
||||||
|
(*LoginResponse)(nil), // 2: chatappgateway.gateway.v1.LoginResponse
|
||||||
|
(*QueryOrderRequest)(nil), // 3: chatappgateway.gateway.v1.QueryOrderRequest
|
||||||
|
(*QueryOrderResponse)(nil), // 4: chatappgateway.gateway.v1.QueryOrderResponse
|
||||||
|
}
|
||||||
|
var file_gateway_proto_depIdxs = []int32{
|
||||||
|
1, // 0: chatappgateway.gateway.v1.LoginResponse.profile:type_name -> chatappgateway.gateway.v1.UserProfile
|
||||||
|
0, // 1: chatappgateway.gateway.v1.ChatAppUser.Login:input_type -> chatappgateway.gateway.v1.LoginRequest
|
||||||
|
3, // 2: chatappgateway.gateway.v1.ChatAppPay.QueryOrder:input_type -> chatappgateway.gateway.v1.QueryOrderRequest
|
||||||
|
2, // 3: chatappgateway.gateway.v1.ChatAppUser.Login:output_type -> chatappgateway.gateway.v1.LoginResponse
|
||||||
|
4, // 4: chatappgateway.gateway.v1.ChatAppPay.QueryOrder:output_type -> chatappgateway.gateway.v1.QueryOrderResponse
|
||||||
|
3, // [3:5] is the sub-list for method output_type
|
||||||
|
1, // [1:3] is the sub-list for method input_type
|
||||||
|
1, // [1:1] is the sub-list for extension type_name
|
||||||
|
1, // [1:1] is the sub-list for extension extendee
|
||||||
|
0, // [0:1] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_gateway_proto_init() }
|
||||||
|
func file_gateway_proto_init() {
|
||||||
|
if File_gateway_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !protoimpl.UnsafeEnabled {
|
||||||
|
file_gateway_proto_msgTypes[0].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*LoginRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_gateway_proto_msgTypes[1].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*UserProfile); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_gateway_proto_msgTypes[2].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*LoginResponse); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_gateway_proto_msgTypes[3].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*QueryOrderRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_gateway_proto_msgTypes[4].Exporter = func(v any, i int) any {
|
||||||
|
switch v := v.(*QueryOrderResponse); 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{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: file_gateway_proto_rawDesc,
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 5,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 2,
|
||||||
|
},
|
||||||
|
GoTypes: file_gateway_proto_goTypes,
|
||||||
|
DependencyIndexes: file_gateway_proto_depIdxs,
|
||||||
|
MessageInfos: file_gateway_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_gateway_proto = out.File
|
||||||
|
file_gateway_proto_rawDesc = nil
|
||||||
|
file_gateway_proto_goTypes = nil
|
||||||
|
file_gateway_proto_depIdxs = nil
|
||||||
|
}
|
||||||
56
api/proto/gateway.proto
Normal file
56
api/proto/gateway.proto
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
package chatappgateway.gateway.v1;
|
||||||
|
|
||||||
|
option go_package = "chatappgateway/api/proto;gatewaypb";
|
||||||
|
|
||||||
|
service ChatAppUser {
|
||||||
|
rpc Login(LoginRequest) returns (LoginResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
service ChatAppPay {
|
||||||
|
rpc QueryOrder(QueryOrderRequest) returns (QueryOrderResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginRequest {
|
||||||
|
string login_type = 1;
|
||||||
|
string account = 2;
|
||||||
|
string password = 3;
|
||||||
|
string country_code = 4;
|
||||||
|
string verify_code = 5;
|
||||||
|
string provider = 6;
|
||||||
|
string provider_token = 7;
|
||||||
|
string device_id = 8;
|
||||||
|
string platform = 9;
|
||||||
|
string app_version = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
message UserProfile {
|
||||||
|
string user_id = 1;
|
||||||
|
string nickname = 2;
|
||||||
|
string avatar_url = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message LoginResponse {
|
||||||
|
string user_id = 1;
|
||||||
|
string access_token = 2;
|
||||||
|
string refresh_token = 3;
|
||||||
|
int64 expires_in = 4;
|
||||||
|
bool is_new_user = 5;
|
||||||
|
UserProfile profile = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message QueryOrderRequest {
|
||||||
|
string order_no = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
message QueryOrderResponse {
|
||||||
|
string order_no = 1;
|
||||||
|
string user_id = 2;
|
||||||
|
string status = 3;
|
||||||
|
string amount = 4;
|
||||||
|
string currency = 5;
|
||||||
|
string subject = 6;
|
||||||
|
string pay_method = 7;
|
||||||
|
string paid_at = 8;
|
||||||
|
}
|
||||||
223
api/proto/gateway_grpc.pb.go
Normal file
223
api/proto/gateway_grpc.pb.go
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.5.1
|
||||||
|
// - protoc v5.29.2
|
||||||
|
// source: gateway.proto
|
||||||
|
|
||||||
|
package gatewaypb
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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.64.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion9
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChatAppUser_Login_FullMethodName = "/chatappgateway.gateway.v1.ChatAppUser/Login"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChatAppUserClient is the client API for ChatAppUser 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 ChatAppUserClient interface {
|
||||||
|
Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type chatAppUserClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChatAppUserClient(cc grpc.ClientConnInterface) ChatAppUserClient {
|
||||||
|
return &chatAppUserClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *chatAppUserClient) Login(ctx context.Context, in *LoginRequest, opts ...grpc.CallOption) (*LoginResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(LoginResponse)
|
||||||
|
err := c.cc.Invoke(ctx, ChatAppUser_Login_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatAppUserServer is the server API for ChatAppUser service.
|
||||||
|
// All implementations must embed UnimplementedChatAppUserServer
|
||||||
|
// for forward compatibility.
|
||||||
|
type ChatAppUserServer interface {
|
||||||
|
Login(context.Context, *LoginRequest) (*LoginResponse, error)
|
||||||
|
mustEmbedUnimplementedChatAppUserServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedChatAppUserServer 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 UnimplementedChatAppUserServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedChatAppUserServer) Login(context.Context, *LoginRequest) (*LoginResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method Login not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedChatAppUserServer) mustEmbedUnimplementedChatAppUserServer() {}
|
||||||
|
func (UnimplementedChatAppUserServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeChatAppUserServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to ChatAppUserServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeChatAppUserServer interface {
|
||||||
|
mustEmbedUnimplementedChatAppUserServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterChatAppUserServer(s grpc.ServiceRegistrar, srv ChatAppUserServer) {
|
||||||
|
// If the following call pancis, it indicates UnimplementedChatAppUserServer 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(&ChatAppUser_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _ChatAppUser_Login_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(LoginRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ChatAppUserServer).Login(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: ChatAppUser_Login_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ChatAppUserServer).Login(ctx, req.(*LoginRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatAppUser_ServiceDesc is the grpc.ServiceDesc for ChatAppUser service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var ChatAppUser_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "chatappgateway.gateway.v1.ChatAppUser",
|
||||||
|
HandlerType: (*ChatAppUserServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "Login",
|
||||||
|
Handler: _ChatAppUser_Login_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "gateway.proto",
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
ChatAppPay_QueryOrder_FullMethodName = "/chatappgateway.gateway.v1.ChatAppPay/QueryOrder"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ChatAppPayClient is the client API for ChatAppPay 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 ChatAppPayClient interface {
|
||||||
|
QueryOrder(ctx context.Context, in *QueryOrderRequest, opts ...grpc.CallOption) (*QueryOrderResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type chatAppPayClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChatAppPayClient(cc grpc.ClientConnInterface) ChatAppPayClient {
|
||||||
|
return &chatAppPayClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *chatAppPayClient) QueryOrder(ctx context.Context, in *QueryOrderRequest, opts ...grpc.CallOption) (*QueryOrderResponse, error) {
|
||||||
|
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
|
||||||
|
out := new(QueryOrderResponse)
|
||||||
|
err := c.cc.Invoke(ctx, ChatAppPay_QueryOrder_FullMethodName, in, out, cOpts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatAppPayServer is the server API for ChatAppPay service.
|
||||||
|
// All implementations must embed UnimplementedChatAppPayServer
|
||||||
|
// for forward compatibility.
|
||||||
|
type ChatAppPayServer interface {
|
||||||
|
QueryOrder(context.Context, *QueryOrderRequest) (*QueryOrderResponse, error)
|
||||||
|
mustEmbedUnimplementedChatAppPayServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedChatAppPayServer 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 UnimplementedChatAppPayServer struct{}
|
||||||
|
|
||||||
|
func (UnimplementedChatAppPayServer) QueryOrder(context.Context, *QueryOrderRequest) (*QueryOrderResponse, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method QueryOrder not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedChatAppPayServer) mustEmbedUnimplementedChatAppPayServer() {}
|
||||||
|
func (UnimplementedChatAppPayServer) testEmbeddedByValue() {}
|
||||||
|
|
||||||
|
// UnsafeChatAppPayServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to ChatAppPayServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeChatAppPayServer interface {
|
||||||
|
mustEmbedUnimplementedChatAppPayServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterChatAppPayServer(s grpc.ServiceRegistrar, srv ChatAppPayServer) {
|
||||||
|
// If the following call pancis, it indicates UnimplementedChatAppPayServer 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(&ChatAppPay_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _ChatAppPay_QueryOrder_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(QueryOrderRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(ChatAppPayServer).QueryOrder(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: ChatAppPay_QueryOrder_FullMethodName,
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(ChatAppPayServer).QueryOrder(ctx, req.(*QueryOrderRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChatAppPay_ServiceDesc is the grpc.ServiceDesc for ChatAppPay service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var ChatAppPay_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "chatappgateway.gateway.v1.ChatAppPay",
|
||||||
|
HandlerType: (*ChatAppPayServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "QueryOrder",
|
||||||
|
Handler: _ChatAppPay_QueryOrder_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "gateway.proto",
|
||||||
|
}
|
||||||
47
cmd/gateway/main.go
Normal file
47
cmd/gateway/main.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"chatappgateway/internal/app"
|
||||||
|
"chatappgateway/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// 允许通过命令行切换配置文件,便于本地、测试和容器环境复用。
|
||||||
|
configPath := flag.String("config", config.DefaultPath, "path to config file")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// 启动阶段先加载配置,避免服务在半初始化状态下对外提供流量。
|
||||||
|
cfg, err := config.Load(*configPath)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("load config failed", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用文本日志,方便直接接入容器日志采集。
|
||||||
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||||
|
Level: slog.LevelInfo,
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 使用进程信号驱动优雅退出。
|
||||||
|
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
application, err := app.New(ctx, cfg, logger)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("build application failed", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
defer application.Close()
|
||||||
|
|
||||||
|
if err := application.Run(ctx); err != nil {
|
||||||
|
logger.Error("application exited with error", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
config/local.yaml
Normal file
14
config/local.yaml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
app:
|
||||||
|
name: chatappgateway
|
||||||
|
env: local
|
||||||
|
http_addr: ":8080"
|
||||||
|
shutdown_timeout: 10s
|
||||||
|
|
||||||
|
# /ready 会使用这些 gRPC 依赖做接流量前检查。
|
||||||
|
grpc:
|
||||||
|
user:
|
||||||
|
target: "127.0.0.1:9001"
|
||||||
|
timeout: 3s
|
||||||
|
pay:
|
||||||
|
target: "127.0.0.1:9002"
|
||||||
|
timeout: 3s
|
||||||
216
docs/ai/第一版 prompt.md
Normal file
216
docs/ai/第一版 prompt.md
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
# ChatAppGateway 第一版 prompt
|
||||||
|
|
||||||
|
你是一名资深 Golang 后端架构师,请为一个海外社交 App 实现一个主业务聚合服务 `ChatAppGateway`。
|
||||||
|
|
||||||
|
## 服务定位
|
||||||
|
|
||||||
|
- `ChatAppGateway` 不是纯反向代理网关,而是 BFF / 聚合编排服务。
|
||||||
|
- 对客户端暴露 HTTP JSON API。
|
||||||
|
- 对内通过 gRPC 调用:
|
||||||
|
- `ChatAppUser.Login`
|
||||||
|
- `ChatAppPay.QueryOrder`
|
||||||
|
- 自身不实现用户域认证规则,也不实现支付域核心规则,只做参数校验、字段归一化、协议适配、统一错误响应、日志与 trace。
|
||||||
|
|
||||||
|
## 目标能力
|
||||||
|
|
||||||
|
第一版只实现三条 HTTP 接口:
|
||||||
|
|
||||||
|
1. `GET /health`
|
||||||
|
2. `GET /ready`
|
||||||
|
3. `POST /api/v1/auth/login`
|
||||||
|
4. `GET /api/v1/pay/orders/{order_no}`
|
||||||
|
|
||||||
|
## 配置要求
|
||||||
|
|
||||||
|
配置文件放在 `config/xxx.yaml`,默认读取 `config/local.yaml`。
|
||||||
|
|
||||||
|
YAML 结构固定如下:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
app:
|
||||||
|
name: chatappgateway
|
||||||
|
env: local
|
||||||
|
http_addr: ":8080"
|
||||||
|
shutdown_timeout: 10s
|
||||||
|
|
||||||
|
grpc:
|
||||||
|
user:
|
||||||
|
target: "127.0.0.1:9001"
|
||||||
|
timeout: 3s
|
||||||
|
pay:
|
||||||
|
target: "127.0.0.1:9002"
|
||||||
|
timeout: 3s
|
||||||
|
```
|
||||||
|
|
||||||
|
必须支持:
|
||||||
|
|
||||||
|
- `-config config/local.yaml` 命令行参数
|
||||||
|
- `gopkg.in/yaml.v3`
|
||||||
|
- 默认值补齐
|
||||||
|
- 配置校验
|
||||||
|
|
||||||
|
## 登录接口要求
|
||||||
|
|
||||||
|
`POST /api/v1/auth/login`
|
||||||
|
|
||||||
|
请求 DTO 至少包含:
|
||||||
|
|
||||||
|
- `login_type`
|
||||||
|
- `account`
|
||||||
|
- `password`
|
||||||
|
- `country_code`
|
||||||
|
- `verify_code`
|
||||||
|
- `provider`
|
||||||
|
- `provider_token`
|
||||||
|
- `device_id`
|
||||||
|
- `platform`
|
||||||
|
- `app_version`
|
||||||
|
|
||||||
|
支持三种登录模式:
|
||||||
|
|
||||||
|
1. `password`
|
||||||
|
- 必填:`account`、`password`
|
||||||
|
2. `sms_code`
|
||||||
|
- 必填:`country_code`、`account`、`verify_code`
|
||||||
|
3. `oauth`
|
||||||
|
- 必填:`provider`、`provider_token`
|
||||||
|
|
||||||
|
网关职责:
|
||||||
|
|
||||||
|
- 只做模式校验和字段归一化
|
||||||
|
- 将请求映射到 `ChatAppUser.Login`
|
||||||
|
- 接收用户服务返回后再统一包装给客户端
|
||||||
|
|
||||||
|
`ChatAppUser.Login` 返回至少包含:
|
||||||
|
|
||||||
|
- `user_id`
|
||||||
|
- `access_token`
|
||||||
|
- `refresh_token`
|
||||||
|
- `expires_in`
|
||||||
|
- `is_new_user`
|
||||||
|
- `profile`
|
||||||
|
|
||||||
|
其中 `profile` 至少包含:
|
||||||
|
|
||||||
|
- `user_id`
|
||||||
|
- `nickname`
|
||||||
|
- `avatar_url`
|
||||||
|
|
||||||
|
## 支付查询接口要求
|
||||||
|
|
||||||
|
`GET /api/v1/pay/orders/{order_no}`
|
||||||
|
|
||||||
|
网关职责:
|
||||||
|
|
||||||
|
- 校验 `order_no` 非空
|
||||||
|
- 调用 `ChatAppPay.QueryOrder`
|
||||||
|
- 把支付服务返回的订单结果转成统一 HTTP JSON 响应
|
||||||
|
|
||||||
|
返回字段至少包含:
|
||||||
|
|
||||||
|
- `order_no`
|
||||||
|
- `user_id`
|
||||||
|
- `status`
|
||||||
|
- `amount`
|
||||||
|
- `currency`
|
||||||
|
- `subject`
|
||||||
|
- `pay_method`
|
||||||
|
- `paid_at`
|
||||||
|
|
||||||
|
## 健康检查要求
|
||||||
|
|
||||||
|
`GET /health`
|
||||||
|
|
||||||
|
- 只做网关自身存活检查
|
||||||
|
- 固定返回 200
|
||||||
|
- 返回:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"service": "chatappgateway"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`GET /ready`
|
||||||
|
|
||||||
|
- 用于 K8s / LB readiness probe
|
||||||
|
- 不是只判断进程活着
|
||||||
|
- 至少要判断:
|
||||||
|
- 配置已经成功加载
|
||||||
|
- 关键依赖已经连通
|
||||||
|
- 服务已经具备接流量能力
|
||||||
|
- 对 `ChatAppGateway` 第一版来说,`/ready` 至少检查:
|
||||||
|
- `ChatAppUser` 的 gRPC 连接可用
|
||||||
|
- `ChatAppPay` 的 gRPC 连接可用
|
||||||
|
- 返回 200 时表示可以接流量,失败时返回 503
|
||||||
|
|
||||||
|
## 错误码和状态码要求
|
||||||
|
|
||||||
|
- 参数错误:`400`
|
||||||
|
- 登录失败或凭证错误:`401`
|
||||||
|
- 订单不存在:`404`
|
||||||
|
- 下游 gRPC 超时:`504`
|
||||||
|
- 下游 gRPC 不可用或内部异常:`502`
|
||||||
|
|
||||||
|
要求统一 JSON 错误结构,例如:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": "bad_request",
|
||||||
|
"message": "login_type is required",
|
||||||
|
"request_id": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 目录结构要求
|
||||||
|
|
||||||
|
项目目录至少包含:
|
||||||
|
|
||||||
|
- `cmd/gateway/main.go`
|
||||||
|
- `internal/config`
|
||||||
|
- `internal/app`
|
||||||
|
- `internal/transport/http`
|
||||||
|
- `internal/integration/usergrpc`
|
||||||
|
- `internal/integration/paygrpc`
|
||||||
|
- `internal/service/auth`
|
||||||
|
- `internal/service/pay`
|
||||||
|
- `api/proto`
|
||||||
|
- `config/local.yaml`
|
||||||
|
|
||||||
|
## 技术要求
|
||||||
|
|
||||||
|
- 使用 idiomatic Go
|
||||||
|
- 使用 `http.ServeMux`
|
||||||
|
- 使用 `slog` 结构化日志
|
||||||
|
- 支持优雅关闭
|
||||||
|
- 收到 `SIGTERM` 后先摘除 `/ready`,再停止接新请求,等待旧请求处理完成后退出
|
||||||
|
- gRPC 连接使用 `insecure.NewCredentials()` 即可
|
||||||
|
- 所有关键代码写中文注释
|
||||||
|
- 不要写成教学 demo,要保持生产风格
|
||||||
|
|
||||||
|
## 测试要求
|
||||||
|
|
||||||
|
至少覆盖以下场景:
|
||||||
|
|
||||||
|
- 配置加载成功
|
||||||
|
- 配置非法时失败
|
||||||
|
- `/health` 返回正确 JSON
|
||||||
|
- `/ready` 在依赖可用时返回 200,在依赖不可用时返回 503
|
||||||
|
- 三种 `login_type` 的参数校验
|
||||||
|
- 登录请求正确映射到 `ChatAppUser.Login`
|
||||||
|
- 订单查询正确映射到 `ChatAppPay.QueryOrder`
|
||||||
|
- user/pay gRPC 超时、不可达、业务错误时的 HTTP 状态码映射
|
||||||
|
|
||||||
|
## 输出顺序要求
|
||||||
|
|
||||||
|
请按以下顺序输出实现内容:
|
||||||
|
|
||||||
|
1. 项目目录结构
|
||||||
|
2. proto 定义
|
||||||
|
3. 配置结构与示例
|
||||||
|
4. HTTP API 设计
|
||||||
|
5. gRPC client 封装
|
||||||
|
6. Go 代码实现
|
||||||
|
7. 测试代码
|
||||||
|
8. 本地运行说明
|
||||||
16
go.mod
Normal file
16
go.mod
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
module chatappgateway
|
||||||
|
|
||||||
|
go 1.23.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
google.golang.org/grpc v1.67.3
|
||||||
|
google.golang.org/protobuf v1.36.11
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
golang.org/x/net v0.35.0 // indirect
|
||||||
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
golang.org/x/text v0.22.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 // indirect
|
||||||
|
)
|
||||||
18
go.sum
Normal file
18
go.sum
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||||
|
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||||
|
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||||
|
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||||
|
google.golang.org/grpc v1.67.3 h1:OgPcDAFKHnH8X3O4WcO4XUc8GRDeKsKReqbQtiCj7N8=
|
||||||
|
google.golang.org/grpc v1.67.3/go.mod h1:YGaHCc6Oap+FzBJTZLBzkGSYt/cvGPFTPxkn7QfSU8s=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
86
internal/app/app.go
Normal file
86
internal/app/app.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
gatewaypb "chatappgateway/api/proto"
|
||||||
|
"chatappgateway/internal/config"
|
||||||
|
"chatappgateway/internal/integration/paygrpc"
|
||||||
|
"chatappgateway/internal/integration/usergrpc"
|
||||||
|
"chatappgateway/internal/service/auth"
|
||||||
|
"chatappgateway/internal/service/pay"
|
||||||
|
httpserver "chatappgateway/internal/transport/http"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Application 组合网关运行所需的全部组件。
|
||||||
|
type Application struct {
|
||||||
|
server *httpserver.Server
|
||||||
|
closers []io.Closer
|
||||||
|
}
|
||||||
|
|
||||||
|
// New 构造完整应用,包括 gRPC 连接、业务服务和 HTTP 服务。
|
||||||
|
func New(ctx context.Context, cfg config.Config, logger *slog.Logger) (*Application, error) {
|
||||||
|
userConn, err := dialGRPC(ctx, cfg.GRPC.User.Target)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("dial ChatAppUser: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payConn, err := dialGRPC(ctx, cfg.GRPC.Pay.Target)
|
||||||
|
if err != nil {
|
||||||
|
_ = userConn.Close()
|
||||||
|
return nil, fmt.Errorf("dial ChatAppPay: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
userClient := usergrpc.New(gatewaypb.NewChatAppUserClient(userConn), cfg.GRPC.User.Timeout)
|
||||||
|
payClient := paygrpc.New(gatewaypb.NewChatAppPayClient(payConn), cfg.GRPC.Pay.Timeout)
|
||||||
|
|
||||||
|
authService := auth.New(userClient)
|
||||||
|
payService := pay.New(payClient)
|
||||||
|
readinessChecker := newReadinessGroup(
|
||||||
|
namedChecker{
|
||||||
|
name: "ChatAppUser",
|
||||||
|
check: grpcConnectionReady(userConn),
|
||||||
|
},
|
||||||
|
namedChecker{
|
||||||
|
name: "ChatAppPay",
|
||||||
|
check: grpcConnectionReady(payConn),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
server := httpserver.New(cfg.App.Name, cfg.App.HTTPAddr, cfg.App.ShutdownTimeout, logger, authService, payService, readinessChecker)
|
||||||
|
|
||||||
|
return &Application{
|
||||||
|
server: server,
|
||||||
|
closers: []io.Closer{userConn, payConn},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run 启动 HTTP 服务。
|
||||||
|
func (a *Application) Run(ctx context.Context) error {
|
||||||
|
return a.server.Run(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close 关闭底层 gRPC 连接。
|
||||||
|
func (a *Application) Close() error {
|
||||||
|
var firstErr error
|
||||||
|
for _, closer := range a.closers {
|
||||||
|
if err := closer.Close(); err != nil && firstErr == nil {
|
||||||
|
firstErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return firstErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func dialGRPC(ctx context.Context, target string) (*grpc.ClientConn, error) {
|
||||||
|
// 网关只做自身健康检查,因此这里使用懒连接,避免下游暂时不可用时阻塞启动。
|
||||||
|
return grpc.DialContext(
|
||||||
|
ctx,
|
||||||
|
target,
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
)
|
||||||
|
}
|
||||||
62
internal/app/readiness.go
Normal file
62
internal/app/readiness.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/connectivity"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadinessChecker 定义统一的就绪检查能力。
|
||||||
|
type ReadinessChecker interface {
|
||||||
|
Ready(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type readinessGroup struct {
|
||||||
|
checkers []namedChecker
|
||||||
|
}
|
||||||
|
|
||||||
|
type namedChecker struct {
|
||||||
|
name string
|
||||||
|
check func(context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// newReadinessGroup 创建一个按顺序执行的就绪检查器组合。
|
||||||
|
func newReadinessGroup(checkers ...namedChecker) ReadinessChecker {
|
||||||
|
return readinessGroup{checkers: checkers}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ready 逐个执行检查,只要有一个失败就返回未就绪。
|
||||||
|
func (g readinessGroup) Ready(ctx context.Context) error {
|
||||||
|
for _, checker := range g.checkers {
|
||||||
|
if err := checker.check(ctx); err != nil {
|
||||||
|
return fmt.Errorf("%s not ready: %w", checker.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func grpcConnectionReady(conn *grpc.ClientConn) func(context.Context) error {
|
||||||
|
return func(ctx context.Context) error {
|
||||||
|
// 主动触发一次连接过程,避免懒连接状态下 readiness 永远不去拨号。
|
||||||
|
conn.Connect()
|
||||||
|
|
||||||
|
for {
|
||||||
|
state := conn.GetState()
|
||||||
|
switch state {
|
||||||
|
case connectivity.Ready:
|
||||||
|
return nil
|
||||||
|
case connectivity.Shutdown:
|
||||||
|
return fmt.Errorf("connection shutdown")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !conn.WaitForStateChange(ctx, state) {
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return fmt.Errorf("state did not change")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
internal/apperr/error.go
Normal file
58
internal/apperr/error.go
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
package apperr
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Error 表示网关内部统一业务错误。
|
||||||
|
type Error struct {
|
||||||
|
Status int
|
||||||
|
Code string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Error) Error() string {
|
||||||
|
return e.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
// New 构造一个统一错误,便于 HTTP 层直接映射。
|
||||||
|
func New(status int, code string, message string) *Error {
|
||||||
|
return &Error{
|
||||||
|
Status: status,
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve 将任意错误转换成统一 HTTP 状态码和错误码。
|
||||||
|
func Resolve(err error) (statusCode int, code string, message string) {
|
||||||
|
if err == nil {
|
||||||
|
return 200, "ok", "ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
var appErr *Error
|
||||||
|
if errors.As(err, &appErr) {
|
||||||
|
return appErr.Status, appErr.Code, appErr.Message
|
||||||
|
}
|
||||||
|
|
||||||
|
grpcStatus, ok := status.FromError(err)
|
||||||
|
if !ok {
|
||||||
|
return 500, "internal_error", "internal server error"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch grpcStatus.Code() {
|
||||||
|
case codes.InvalidArgument:
|
||||||
|
return 400, "bad_request", grpcStatus.Message()
|
||||||
|
case codes.Unauthenticated, codes.PermissionDenied:
|
||||||
|
return 401, "unauthorized", grpcStatus.Message()
|
||||||
|
case codes.NotFound:
|
||||||
|
return 404, "not_found", grpcStatus.Message()
|
||||||
|
case codes.DeadlineExceeded:
|
||||||
|
return 504, "upstream_timeout", "upstream request timeout"
|
||||||
|
default:
|
||||||
|
return 502, "upstream_error", grpcStatus.Message()
|
||||||
|
}
|
||||||
|
}
|
||||||
116
internal/config/config.go
Normal file
116
internal/config/config.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
const DefaultPath = "config/local.yaml"
|
||||||
|
|
||||||
|
// Config 汇总整个网关服务的运行参数。
|
||||||
|
type Config struct {
|
||||||
|
App AppConfig `yaml:"app"`
|
||||||
|
GRPC GRPCConfig `yaml:"grpc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppConfig 描述应用自身参数。
|
||||||
|
type AppConfig struct {
|
||||||
|
Name string `yaml:"name"`
|
||||||
|
Env string `yaml:"env"`
|
||||||
|
HTTPAddr string `yaml:"http_addr"`
|
||||||
|
ShutdownTimeout time.Duration `yaml:"shutdown_timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GRPCConfig 聚合所有下游 gRPC 服务配置。
|
||||||
|
type GRPCConfig struct {
|
||||||
|
User UpstreamConfig `yaml:"user"`
|
||||||
|
Pay UpstreamConfig `yaml:"pay"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpstreamConfig 描述单个下游服务的地址和超时。
|
||||||
|
type UpstreamConfig struct {
|
||||||
|
Target string `yaml:"target"`
|
||||||
|
Timeout time.Duration `yaml:"timeout"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load 从 YAML 文件加载配置,并补齐默认值和校验。
|
||||||
|
func Load(path string) (Config, error) {
|
||||||
|
// 先读取配置文件内容。
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return Config{}, fmt.Errorf("read config file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用 KnownFields 防止配置拼写错误悄悄溜过。
|
||||||
|
cfg := defaultConfig()
|
||||||
|
decoder := yaml.NewDecoder(bytes.NewReader(data))
|
||||||
|
decoder.KnownFields(true)
|
||||||
|
if err := decoder.Decode(&cfg); err != nil {
|
||||||
|
return Config{}, fmt.Errorf("decode config file %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := validate(cfg); err != nil {
|
||||||
|
return Config{}, err
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultConfig() Config {
|
||||||
|
return Config{
|
||||||
|
App: AppConfig{
|
||||||
|
Name: "chatappgateway",
|
||||||
|
Env: "local",
|
||||||
|
HTTPAddr: ":8080",
|
||||||
|
ShutdownTimeout: 10 * time.Second,
|
||||||
|
},
|
||||||
|
GRPC: GRPCConfig{
|
||||||
|
User: UpstreamConfig{
|
||||||
|
Target: "127.0.0.1:9001",
|
||||||
|
Timeout: 3 * time.Second,
|
||||||
|
},
|
||||||
|
Pay: UpstreamConfig{
|
||||||
|
Target: "127.0.0.1:9002",
|
||||||
|
Timeout: 3 * time.Second,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validate(cfg Config) error {
|
||||||
|
// 应用名用于日志标签,不能为空。
|
||||||
|
if cfg.App.Name == "" {
|
||||||
|
return fmt.Errorf("app.name is required")
|
||||||
|
}
|
||||||
|
// 监听地址必须能被 TCP 地址解析。
|
||||||
|
if _, err := net.ResolveTCPAddr("tcp", cfg.App.HTTPAddr); err != nil {
|
||||||
|
return fmt.Errorf("app.http_addr is invalid: %w", err)
|
||||||
|
}
|
||||||
|
if cfg.App.ShutdownTimeout <= 0 {
|
||||||
|
return fmt.Errorf("app.shutdown_timeout must be greater than 0")
|
||||||
|
}
|
||||||
|
if err := validateUpstream("grpc.user", cfg.GRPC.User); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := validateUpstream("grpc.pay", cfg.GRPC.Pay); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateUpstream(name string, cfg UpstreamConfig) error {
|
||||||
|
if cfg.Target == "" {
|
||||||
|
return fmt.Errorf("%s.target is required", name)
|
||||||
|
}
|
||||||
|
if _, err := net.ResolveTCPAddr("tcp", cfg.Target); err != nil {
|
||||||
|
return fmt.Errorf("%s.target is invalid: %w", name, err)
|
||||||
|
}
|
||||||
|
if cfg.Timeout <= 0 {
|
||||||
|
return fmt.Errorf("%s.timeout must be greater than 0", name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
89
internal/config/config_test.go
Normal file
89
internal/config/config_test.go
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoadLocalConfig(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
cfg, err := Load(filepath.Join("..", "..", "config", "local.yaml"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.App.Name != "chatappgateway" {
|
||||||
|
t.Fatalf("unexpected app name: %s", cfg.App.Name)
|
||||||
|
}
|
||||||
|
if cfg.App.HTTPAddr != ":8080" {
|
||||||
|
t.Fatalf("unexpected http addr: %s", cfg.App.HTTPAddr)
|
||||||
|
}
|
||||||
|
if cfg.GRPC.User.Target != "127.0.0.1:9001" {
|
||||||
|
t.Fatalf("unexpected user target: %s", cfg.GRPC.User.Target)
|
||||||
|
}
|
||||||
|
if cfg.GRPC.Pay.Timeout != 3*time.Second {
|
||||||
|
t.Fatalf("unexpected pay timeout: %s", cfg.GRPC.Pay.Timeout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadInvalidConfig(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "invalid.yaml")
|
||||||
|
content := strings.TrimSpace(`
|
||||||
|
app:
|
||||||
|
name: chatappgateway
|
||||||
|
http_addr: "bad-address"
|
||||||
|
grpc:
|
||||||
|
user:
|
||||||
|
target: ""
|
||||||
|
timeout: 3s
|
||||||
|
pay:
|
||||||
|
target: "127.0.0.1:9002"
|
||||||
|
timeout: 0s
|
||||||
|
`)
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := Load(path)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Load returned nil error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "app.http_addr is invalid") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadInvalidUpstream(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
path := filepath.Join(t.TempDir(), "invalid-upstream.yaml")
|
||||||
|
content := strings.TrimSpace(`
|
||||||
|
app:
|
||||||
|
name: chatappgateway
|
||||||
|
http_addr: ":8080"
|
||||||
|
grpc:
|
||||||
|
user:
|
||||||
|
target: "127.0.0.1:9001"
|
||||||
|
timeout: 3s
|
||||||
|
pay:
|
||||||
|
target: "invalid"
|
||||||
|
timeout: 3s
|
||||||
|
`)
|
||||||
|
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
|
||||||
|
t.Fatalf("WriteFile returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := Load(path)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Load returned nil error")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "grpc.pay.target is invalid") {
|
||||||
|
t.Fatalf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
30
internal/integration/paygrpc/client.go
Normal file
30
internal/integration/paygrpc/client.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package paygrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
gatewaypb "chatappgateway/api/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client 封装支付服务 gRPC client,并统一超时控制。
|
||||||
|
type Client struct {
|
||||||
|
timeout time.Duration
|
||||||
|
client gatewaypb.ChatAppPayClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// New 根据底层 gRPC client 构造支付服务调用器。
|
||||||
|
func New(client gatewaypb.ChatAppPayClient, timeout time.Duration) *Client {
|
||||||
|
return &Client{
|
||||||
|
timeout: timeout,
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryOrder 调用支付服务最小订单查询接口。
|
||||||
|
func (c *Client) QueryOrder(ctx context.Context, request *gatewaypb.QueryOrderRequest) (*gatewaypb.QueryOrderResponse, error) {
|
||||||
|
callCtx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
return c.client.QueryOrder(callCtx, request)
|
||||||
|
}
|
||||||
30
internal/integration/usergrpc/client.go
Normal file
30
internal/integration/usergrpc/client.go
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
package usergrpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
gatewaypb "chatappgateway/api/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client 封装用户服务 gRPC client,并统一超时控制。
|
||||||
|
type Client struct {
|
||||||
|
timeout time.Duration
|
||||||
|
client gatewaypb.ChatAppUserClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// New 根据底层 gRPC client 构造用户服务调用器。
|
||||||
|
func New(client gatewaypb.ChatAppUserClient, timeout time.Duration) *Client {
|
||||||
|
return &Client{
|
||||||
|
timeout: timeout,
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login 调用用户服务登录接口。
|
||||||
|
func (c *Client) Login(ctx context.Context, request *gatewaypb.LoginRequest) (*gatewaypb.LoginResponse, error) {
|
||||||
|
callCtx, cancel := context.WithTimeout(ctx, c.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
return c.client.Login(callCtx, request)
|
||||||
|
}
|
||||||
107
internal/service/auth/service.go
Normal file
107
internal/service/auth/service.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
gatewaypb "chatappgateway/api/proto"
|
||||||
|
"chatappgateway/internal/apperr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client 定义用户服务 gRPC 客户端能力。
|
||||||
|
type Client interface {
|
||||||
|
Login(ctx context.Context, request *gatewaypb.LoginRequest) (*gatewaypb.LoginResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service 负责登录参数校验、字段归一化和下游调用。
|
||||||
|
type Service struct {
|
||||||
|
client Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoginRequest 描述 HTTP 层的登录入参。
|
||||||
|
type LoginRequest struct {
|
||||||
|
LoginType string `json:"login_type"`
|
||||||
|
Account string `json:"account"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
CountryCode string `json:"country_code"`
|
||||||
|
VerifyCode string `json:"verify_code"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
ProviderToken string `json:"provider_token"`
|
||||||
|
DeviceID string `json:"device_id"`
|
||||||
|
Platform string `json:"platform"`
|
||||||
|
AppVersion string `json:"app_version"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// New 创建登录服务。
|
||||||
|
func New(client Client) *Service {
|
||||||
|
return &Service{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Login 校验客户端请求并转成 gRPC 请求。
|
||||||
|
func (s *Service) Login(ctx context.Context, request LoginRequest) (*gatewaypb.LoginResponse, error) {
|
||||||
|
normalized, err := normalize(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.client.Login(ctx, &gatewaypb.LoginRequest{
|
||||||
|
LoginType: normalized.LoginType,
|
||||||
|
Account: normalized.Account,
|
||||||
|
Password: normalized.Password,
|
||||||
|
CountryCode: normalized.CountryCode,
|
||||||
|
VerifyCode: normalized.VerifyCode,
|
||||||
|
Provider: normalized.Provider,
|
||||||
|
ProviderToken: normalized.ProviderToken,
|
||||||
|
DeviceId: normalized.DeviceID,
|
||||||
|
Platform: normalized.Platform,
|
||||||
|
AppVersion: normalized.AppVersion,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalize(request LoginRequest) (LoginRequest, error) {
|
||||||
|
normalized := LoginRequest{
|
||||||
|
LoginType: strings.ToLower(strings.TrimSpace(request.LoginType)),
|
||||||
|
Account: strings.TrimSpace(request.Account),
|
||||||
|
Password: request.Password,
|
||||||
|
CountryCode: strings.TrimSpace(request.CountryCode),
|
||||||
|
VerifyCode: strings.TrimSpace(request.VerifyCode),
|
||||||
|
Provider: strings.ToLower(strings.TrimSpace(request.Provider)),
|
||||||
|
ProviderToken: strings.TrimSpace(request.ProviderToken),
|
||||||
|
DeviceID: strings.TrimSpace(request.DeviceID),
|
||||||
|
Platform: strings.TrimSpace(request.Platform),
|
||||||
|
AppVersion: strings.TrimSpace(request.AppVersion),
|
||||||
|
}
|
||||||
|
|
||||||
|
switch normalized.LoginType {
|
||||||
|
case "":
|
||||||
|
return LoginRequest{}, apperr.New(400, "bad_request", "login_type is required")
|
||||||
|
case "password":
|
||||||
|
if normalized.Account == "" {
|
||||||
|
return LoginRequest{}, apperr.New(400, "bad_request", "account is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(normalized.Password) == "" {
|
||||||
|
return LoginRequest{}, apperr.New(400, "bad_request", "password is required")
|
||||||
|
}
|
||||||
|
case "sms_code":
|
||||||
|
if normalized.CountryCode == "" {
|
||||||
|
return LoginRequest{}, apperr.New(400, "bad_request", "country_code is required")
|
||||||
|
}
|
||||||
|
if normalized.Account == "" {
|
||||||
|
return LoginRequest{}, apperr.New(400, "bad_request", "account is required")
|
||||||
|
}
|
||||||
|
if normalized.VerifyCode == "" {
|
||||||
|
return LoginRequest{}, apperr.New(400, "bad_request", "verify_code is required")
|
||||||
|
}
|
||||||
|
case "oauth":
|
||||||
|
if normalized.Provider == "" {
|
||||||
|
return LoginRequest{}, apperr.New(400, "bad_request", "provider is required")
|
||||||
|
}
|
||||||
|
if normalized.ProviderToken == "" {
|
||||||
|
return LoginRequest{}, apperr.New(400, "bad_request", "provider_token is required")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return LoginRequest{}, apperr.New(400, "bad_request", "login_type must be one of password, sms_code, oauth")
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalized, nil
|
||||||
|
}
|
||||||
36
internal/service/pay/service.go
Normal file
36
internal/service/pay/service.go
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
package pay
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
gatewaypb "chatappgateway/api/proto"
|
||||||
|
"chatappgateway/internal/apperr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Client 定义支付服务 gRPC 客户端能力。
|
||||||
|
type Client interface {
|
||||||
|
QueryOrder(ctx context.Context, request *gatewaypb.QueryOrderRequest) (*gatewaypb.QueryOrderResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service 负责订单号校验和下游支付查询。
|
||||||
|
type Service struct {
|
||||||
|
client Client
|
||||||
|
}
|
||||||
|
|
||||||
|
// New 创建支付查询服务。
|
||||||
|
func New(client Client) *Service {
|
||||||
|
return &Service{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
// QueryOrder 校验订单号并调用支付服务。
|
||||||
|
func (s *Service) QueryOrder(ctx context.Context, orderNo string) (*gatewaypb.QueryOrderResponse, error) {
|
||||||
|
normalized := strings.TrimSpace(orderNo)
|
||||||
|
if normalized == "" {
|
||||||
|
return nil, apperr.New(400, "bad_request", "order_no is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.client.QueryOrder(ctx, &gatewaypb.QueryOrderRequest{
|
||||||
|
OrderNo: normalized,
|
||||||
|
})
|
||||||
|
}
|
||||||
270
internal/transport/http/server.go
Normal file
270
internal/transport/http/server.go
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
package httpserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
gatewaypb "chatappgateway/api/proto"
|
||||||
|
"chatappgateway/internal/apperr"
|
||||||
|
"chatappgateway/internal/service/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
const orderPrefix = "/api/v1/pay/orders/"
|
||||||
|
|
||||||
|
type requestIDKey struct{}
|
||||||
|
|
||||||
|
// AuthService 定义 HTTP 层依赖的登录业务能力。
|
||||||
|
type AuthService interface {
|
||||||
|
Login(ctx context.Context, request auth.LoginRequest) (*gatewaypb.LoginResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PayService 定义 HTTP 层依赖的支付查询能力。
|
||||||
|
type PayService interface {
|
||||||
|
QueryOrder(ctx context.Context, orderNo string) (*gatewaypb.QueryOrderResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadinessChecker 定义就绪检查能力,只有可接流量时才返回 nil。
|
||||||
|
type ReadinessChecker interface {
|
||||||
|
Ready(ctx context.Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server 包装整个 HTTP 服务。
|
||||||
|
type Server struct {
|
||||||
|
appName string
|
||||||
|
addr string
|
||||||
|
shutdownTimeout time.Duration
|
||||||
|
logger *slog.Logger
|
||||||
|
authService AuthService
|
||||||
|
payService PayService
|
||||||
|
readiness ReadinessChecker
|
||||||
|
handler http.Handler
|
||||||
|
ready atomic.Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// New 创建 HTTP 服务实例。
|
||||||
|
func New(appName string, addr string, shutdownTimeout time.Duration, logger *slog.Logger, authService AuthService, payService PayService, readiness ReadinessChecker) *Server {
|
||||||
|
server := &Server{
|
||||||
|
appName: appName,
|
||||||
|
addr: addr,
|
||||||
|
shutdownTimeout: shutdownTimeout,
|
||||||
|
logger: logger,
|
||||||
|
authService: authService,
|
||||||
|
payService: payService,
|
||||||
|
readiness: readiness,
|
||||||
|
}
|
||||||
|
server.ready.Store(true)
|
||||||
|
server.handler = server.withRequestContext(server.routes())
|
||||||
|
return server
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handler 返回可供测试复用的 HTTP handler。
|
||||||
|
func (s *Server) Handler() http.Handler {
|
||||||
|
return s.handler
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run 启动 HTTP 服务并在上下文取消后优雅关闭。
|
||||||
|
func (s *Server) Run(ctx context.Context) error {
|
||||||
|
httpServer := &http.Server{
|
||||||
|
Addr: s.addr,
|
||||||
|
Handler: s.handler,
|
||||||
|
ReadHeaderTimeout: 5 * time.Second,
|
||||||
|
ReadTimeout: 10 * time.Second,
|
||||||
|
WriteTimeout: 10 * time.Second,
|
||||||
|
IdleTimeout: 60 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
// 先摘除 readiness,通知上游停止继续发送新流量。
|
||||||
|
s.ready.Store(false)
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), s.shutdownTimeout)
|
||||||
|
defer cancel()
|
||||||
|
_ = httpServer.Shutdown(shutdownCtx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
s.logger.Info("http server started", "addr", s.addr)
|
||||||
|
|
||||||
|
err := httpServer.ListenAndServe()
|
||||||
|
if errors.Is(err, http.ErrServerClosed) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) routes() http.Handler {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/health", s.handleHealth)
|
||||||
|
mux.HandleFunc("/ready", s.handleReady)
|
||||||
|
mux.HandleFunc("/api/v1/auth/login", s.handleLogin)
|
||||||
|
mux.HandleFunc(orderPrefix, s.handleQueryOrder)
|
||||||
|
mux.HandleFunc("/", s.handleNotFound)
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleHealth(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
writeJSON(w, http.StatusOK, map[string]string{
|
||||||
|
"status": "ok",
|
||||||
|
"service": s.appName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if !s.ready.Load() {
|
||||||
|
writeError(w, requestIDFromContext(r.Context()), apperr.New(http.StatusServiceUnavailable, "not_ready", "service is shutting down"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.readiness != nil {
|
||||||
|
checkCtx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := s.readiness.Ready(checkCtx); err != nil {
|
||||||
|
writeError(w, requestIDFromContext(r.Context()), apperr.New(http.StatusServiceUnavailable, "not_ready", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, map[string]string{
|
||||||
|
"status": "ready",
|
||||||
|
"service": s.appName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodPost {
|
||||||
|
writeError(w, requestIDFromContext(r.Context()), apperr.New(http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var request auth.LoginRequest
|
||||||
|
decoder := json.NewDecoder(r.Body)
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
if err := decoder.Decode(&request); err != nil {
|
||||||
|
writeError(w, requestIDFromContext(r.Context()), apperr.New(http.StatusBadRequest, "bad_request", "invalid json body"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := s.authService.Login(r.Context(), request)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, requestIDFromContext(r.Context()), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeEnvelope(w, http.StatusOK, requestIDFromContext(r.Context()), response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleQueryOrder(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
writeError(w, requestIDFromContext(r.Context()), apperr.New(http.StatusMethodNotAllowed, "method_not_allowed", "method not allowed"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
orderNo, ok := extractOrderNo(r.URL.Path)
|
||||||
|
if !ok {
|
||||||
|
writeError(w, requestIDFromContext(r.Context()), apperr.New(http.StatusNotFound, "not_found", "order not found"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := s.payService.QueryOrder(r.Context(), orderNo)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, requestIDFromContext(r.Context()), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeEnvelope(w, http.StatusOK, requestIDFromContext(r.Context()), response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleNotFound(w http.ResponseWriter, r *http.Request) {
|
||||||
|
writeError(w, requestIDFromContext(r.Context()), apperr.New(http.StatusNotFound, "not_found", "route not found"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) withRequestContext(next http.Handler) http.Handler {
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
requestID := strings.TrimSpace(r.Header.Get("X-Request-Id"))
|
||||||
|
if requestID == "" {
|
||||||
|
requestID = newRequestID()
|
||||||
|
}
|
||||||
|
w.Header().Set("X-Request-Id", requestID)
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
writer := &statusCapturingResponseWriter{ResponseWriter: w, statusCode: http.StatusOK}
|
||||||
|
ctx := context.WithValue(r.Context(), requestIDKey{}, requestID)
|
||||||
|
next.ServeHTTP(writer, r.WithContext(ctx))
|
||||||
|
|
||||||
|
s.logger.Info("http request completed",
|
||||||
|
"request_id", requestID,
|
||||||
|
"method", r.Method,
|
||||||
|
"path", r.URL.Path,
|
||||||
|
"status", writer.statusCode,
|
||||||
|
"duration", time.Since(start),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeEnvelope(w http.ResponseWriter, statusCode int, requestID string, data any) {
|
||||||
|
writeJSON(w, statusCode, map[string]any{
|
||||||
|
"code": "ok",
|
||||||
|
"message": "ok",
|
||||||
|
"request_id": requestID,
|
||||||
|
"data": data,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeError(w http.ResponseWriter, requestID string, err error) {
|
||||||
|
statusCode, code, message := apperr.Resolve(err)
|
||||||
|
writeJSON(w, statusCode, map[string]any{
|
||||||
|
"code": code,
|
||||||
|
"message": message,
|
||||||
|
"request_id": requestID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSON(w http.ResponseWriter, statusCode int, payload any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(statusCode)
|
||||||
|
_ = json.NewEncoder(w).Encode(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractOrderNo(path string) (string, bool) {
|
||||||
|
if !strings.HasPrefix(path, orderPrefix) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
orderNo := strings.TrimPrefix(path, orderPrefix)
|
||||||
|
if orderNo == "" || strings.Contains(orderNo, "/") {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
return orderNo, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func requestIDFromContext(ctx context.Context) string {
|
||||||
|
requestID, _ := ctx.Value(requestIDKey{}).(string)
|
||||||
|
return requestID
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRequestID() string {
|
||||||
|
// 优先使用随机值,避免在高并发下出现碰撞。
|
||||||
|
buffer := make([]byte, 8)
|
||||||
|
if _, err := io.ReadFull(rand.Reader, buffer); err == nil {
|
||||||
|
return hex.EncodeToString(buffer)
|
||||||
|
}
|
||||||
|
return strconv.FormatInt(time.Now().UnixNano(), 36)
|
||||||
|
}
|
||||||
|
|
||||||
|
type statusCapturingResponseWriter struct {
|
||||||
|
http.ResponseWriter
|
||||||
|
statusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *statusCapturingResponseWriter) WriteHeader(statusCode int) {
|
||||||
|
w.statusCode = statusCode
|
||||||
|
w.ResponseWriter.WriteHeader(statusCode)
|
||||||
|
}
|
||||||
555
internal/transport/http/server_test.go
Normal file
555
internal/transport/http/server_test.go
Normal file
@ -0,0 +1,555 @@
|
|||||||
|
package httpserver_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"log/slog"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
gatewaypb "chatappgateway/api/proto"
|
||||||
|
"chatappgateway/internal/integration/paygrpc"
|
||||||
|
"chatappgateway/internal/integration/usergrpc"
|
||||||
|
"chatappgateway/internal/service/auth"
|
||||||
|
"chatappgateway/internal/service/pay"
|
||||||
|
httpserver "chatappgateway/internal/transport/http"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
"google.golang.org/grpc/test/bufconn"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHealth(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newTestEnv(t, 200*time.Millisecond, 200*time.Millisecond)
|
||||||
|
defer env.Close()
|
||||||
|
|
||||||
|
resp, body := doRequest(t, env.server.Client(), http.MethodGet, env.server.URL+"/health", nil)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("unexpected status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload map[string]string
|
||||||
|
if err := json.Unmarshal(body, &payload); err != nil {
|
||||||
|
t.Fatalf("Unmarshal returned error: %v", err)
|
||||||
|
}
|
||||||
|
if payload["status"] != "ok" || payload["service"] != "chatappgateway" {
|
||||||
|
t.Fatalf("unexpected payload: %#v", payload)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReady(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("ready success", func(t *testing.T) {
|
||||||
|
env := newTestEnv(t, 200*time.Millisecond, 200*time.Millisecond)
|
||||||
|
defer env.Close()
|
||||||
|
|
||||||
|
resp, body := doRequest(t, env.server.Client(), http.MethodGet, env.server.URL+"/ready", nil)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("unexpected status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload map[string]string
|
||||||
|
if err := json.Unmarshal(body, &payload); err != nil {
|
||||||
|
t.Fatalf("Unmarshal returned error: %v", err)
|
||||||
|
}
|
||||||
|
if payload["status"] != "ready" || payload["service"] != "chatappgateway" {
|
||||||
|
t.Fatalf("unexpected payload: %#v", payload)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("not ready", func(t *testing.T) {
|
||||||
|
env := newTestEnv(t, 200*time.Millisecond, 200*time.Millisecond)
|
||||||
|
defer env.Close()
|
||||||
|
|
||||||
|
env.readinessChecker.err = errors.New("redis not ready")
|
||||||
|
|
||||||
|
resp, responseBody := doRequest(t, env.server.Client(), http.MethodGet, env.server.URL+"/ready", nil)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assertErrorResponse(t, resp.StatusCode, responseBody, http.StatusServiceUnavailable, "redis not ready")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginValidation(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newTestEnv(t, 200*time.Millisecond, 200*time.Millisecond)
|
||||||
|
defer env.Close()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
body string
|
||||||
|
message string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "password missing account",
|
||||||
|
body: `{"login_type":"password","password":"secret"}`,
|
||||||
|
message: "account is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "sms_code missing verify_code",
|
||||||
|
body: `{"login_type":"sms_code","country_code":"+86","account":"13800138000"}`,
|
||||||
|
message: "verify_code is required",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "oauth missing provider_token",
|
||||||
|
body: `{"login_type":"oauth","provider":"google"}`,
|
||||||
|
message: "provider_token is required",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
resp, body := doRequest(t, env.server.Client(), http.MethodPost, env.server.URL+"/api/v1/auth/login", strings.NewReader(testCase.body))
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusBadRequest {
|
||||||
|
t.Fatalf("unexpected status: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload errorResponse
|
||||||
|
if err := json.Unmarshal(body, &payload); err != nil {
|
||||||
|
t.Fatalf("Unmarshal returned error: %v", err)
|
||||||
|
}
|
||||||
|
if payload.Message != testCase.message {
|
||||||
|
t.Fatalf("unexpected message: %s", payload.Message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if env.userServer.CallCount() != 0 {
|
||||||
|
t.Fatalf("expected user service not to be called, got %d", env.userServer.CallCount())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoginDelegatesToUserGRPC(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newTestEnv(t, 200*time.Millisecond, 200*time.Millisecond)
|
||||||
|
defer env.Close()
|
||||||
|
|
||||||
|
env.userServer.response = &gatewaypb.LoginResponse{
|
||||||
|
UserId: "u-100",
|
||||||
|
AccessToken: "access-token",
|
||||||
|
RefreshToken: "refresh-token",
|
||||||
|
ExpiresIn: 7200,
|
||||||
|
IsNewUser: true,
|
||||||
|
Profile: &gatewaypb.UserProfile{
|
||||||
|
UserId: "u-100",
|
||||||
|
Nickname: "Neo",
|
||||||
|
AvatarUrl: "https://example.com/avatar.png",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
body := `{
|
||||||
|
"login_type":" PASSWORD ",
|
||||||
|
"account":" demo@example.com ",
|
||||||
|
"password":"secret",
|
||||||
|
"device_id":" dev-1 ",
|
||||||
|
"platform":" ios ",
|
||||||
|
"app_version":" 1.0.0 "
|
||||||
|
}`
|
||||||
|
|
||||||
|
resp, responseBody := doRequest(t, env.server.Client(), http.MethodPost, env.server.URL+"/api/v1/auth/login", strings.NewReader(body))
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("unexpected status: %d body=%s", resp.StatusCode, string(responseBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload successResponse[gatewaypb.LoginResponse]
|
||||||
|
if err := json.Unmarshal(responseBody, &payload); err != nil {
|
||||||
|
t.Fatalf("Unmarshal returned error: %v", err)
|
||||||
|
}
|
||||||
|
if payload.Data.UserId != "u-100" {
|
||||||
|
t.Fatalf("unexpected user id: %s", payload.Data.UserId)
|
||||||
|
}
|
||||||
|
|
||||||
|
request := env.userServer.LastRequest()
|
||||||
|
if request == nil {
|
||||||
|
t.Fatal("expected request to be captured")
|
||||||
|
}
|
||||||
|
if request.LoginType != "password" {
|
||||||
|
t.Fatalf("unexpected login type: %s", request.LoginType)
|
||||||
|
}
|
||||||
|
if request.Account != "demo@example.com" {
|
||||||
|
t.Fatalf("unexpected account: %q", request.Account)
|
||||||
|
}
|
||||||
|
if request.DeviceId != "dev-1" {
|
||||||
|
t.Fatalf("unexpected device id: %q", request.DeviceId)
|
||||||
|
}
|
||||||
|
if request.Platform != "ios" {
|
||||||
|
t.Fatalf("unexpected platform: %q", request.Platform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQueryOrderDelegatesToPayGRPC(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newTestEnv(t, 200*time.Millisecond, 200*time.Millisecond)
|
||||||
|
defer env.Close()
|
||||||
|
|
||||||
|
env.payServer.response = &gatewaypb.QueryOrderResponse{
|
||||||
|
OrderNo: "order-001",
|
||||||
|
UserId: "u-100",
|
||||||
|
Status: "paid",
|
||||||
|
Amount: "9.99",
|
||||||
|
Currency: "USD",
|
||||||
|
Subject: "VIP",
|
||||||
|
PayMethod: "apple_pay",
|
||||||
|
PaidAt: "2026-04-04T12:00:00Z",
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, responseBody := doRequest(t, env.server.Client(), http.MethodGet, env.server.URL+"/api/v1/pay/orders/order-001", nil)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
t.Fatalf("unexpected status: %d body=%s", resp.StatusCode, string(responseBody))
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload successResponse[gatewaypb.QueryOrderResponse]
|
||||||
|
if err := json.Unmarshal(responseBody, &payload); err != nil {
|
||||||
|
t.Fatalf("Unmarshal returned error: %v", err)
|
||||||
|
}
|
||||||
|
if payload.Data.OrderNo != "order-001" {
|
||||||
|
t.Fatalf("unexpected order no: %s", payload.Data.OrderNo)
|
||||||
|
}
|
||||||
|
|
||||||
|
request := env.payServer.LastRequest()
|
||||||
|
if request == nil || request.OrderNo != "order-001" {
|
||||||
|
t.Fatalf("unexpected pay request: %#v", request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrorMapping(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
t.Run("login unauthorized", func(t *testing.T) {
|
||||||
|
env := newTestEnv(t, 200*time.Millisecond, 200*time.Millisecond)
|
||||||
|
defer env.Close()
|
||||||
|
|
||||||
|
env.userServer.err = status.Error(codes.Unauthenticated, "invalid credentials")
|
||||||
|
|
||||||
|
resp, responseBody := doRequest(t, env.server.Client(), http.MethodPost, env.server.URL+"/api/v1/auth/login", strings.NewReader(`{"login_type":"password","account":"demo","password":"wrong"}`))
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assertErrorResponse(t, resp.StatusCode, responseBody, http.StatusUnauthorized, "invalid credentials")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("pay not found", func(t *testing.T) {
|
||||||
|
env := newTestEnv(t, 200*time.Millisecond, 200*time.Millisecond)
|
||||||
|
defer env.Close()
|
||||||
|
|
||||||
|
env.payServer.err = status.Error(codes.NotFound, "order not found")
|
||||||
|
|
||||||
|
resp, responseBody := doRequest(t, env.server.Client(), http.MethodGet, env.server.URL+"/api/v1/pay/orders/missing-order", nil)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assertErrorResponse(t, resp.StatusCode, responseBody, http.StatusNotFound, "order not found")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("login timeout", func(t *testing.T) {
|
||||||
|
env := newTestEnv(t, 20*time.Millisecond, 200*time.Millisecond)
|
||||||
|
defer env.Close()
|
||||||
|
|
||||||
|
env.userServer.delay = 150 * time.Millisecond
|
||||||
|
|
||||||
|
resp, responseBody := doRequest(t, env.server.Client(), http.MethodPost, env.server.URL+"/api/v1/auth/login", strings.NewReader(`{"login_type":"password","account":"demo","password":"secret"}`))
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assertErrorResponse(t, resp.StatusCode, responseBody, http.StatusGatewayTimeout, "upstream request timeout")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("pay unavailable", func(t *testing.T) {
|
||||||
|
env := newTestEnv(t, 200*time.Millisecond, 200*time.Millisecond)
|
||||||
|
defer env.Close()
|
||||||
|
|
||||||
|
env.payServer.err = status.Error(codes.Unavailable, "pay service unavailable")
|
||||||
|
|
||||||
|
resp, responseBody := doRequest(t, env.server.Client(), http.MethodGet, env.server.URL+"/api/v1/pay/orders/order-001", nil)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assertErrorResponse(t, resp.StatusCode, responseBody, http.StatusBadGateway, "pay service unavailable")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRouteNotFoundReturnsJSON(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
env := newTestEnv(t, 200*time.Millisecond, 200*time.Millisecond)
|
||||||
|
defer env.Close()
|
||||||
|
|
||||||
|
resp, responseBody := doRequest(t, env.server.Client(), http.MethodGet, env.server.URL+"/not-exists", nil)
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
assertErrorResponse(t, resp.StatusCode, responseBody, http.StatusNotFound, "route not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
type testEnv struct {
|
||||||
|
server *httptest.Server
|
||||||
|
userServer *mockUserServer
|
||||||
|
payServer *mockPayServer
|
||||||
|
readinessChecker *mockReadinessChecker
|
||||||
|
closeFn func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *testEnv) Close() {
|
||||||
|
e.server.Close()
|
||||||
|
e.closeFn()
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestEnv(t *testing.T, userTimeout time.Duration, payTimeout time.Duration) *testEnv {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
userServer := &mockUserServer{
|
||||||
|
response: &gatewaypb.LoginResponse{
|
||||||
|
UserId: "default-user",
|
||||||
|
AccessToken: "default-access",
|
||||||
|
RefreshToken: "default-refresh",
|
||||||
|
ExpiresIn: 3600,
|
||||||
|
Profile: &gatewaypb.UserProfile{
|
||||||
|
UserId: "default-user",
|
||||||
|
Nickname: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
payServer := &mockPayServer{
|
||||||
|
response: &gatewaypb.QueryOrderResponse{
|
||||||
|
OrderNo: "default-order",
|
||||||
|
UserId: "default-user",
|
||||||
|
Status: "pending",
|
||||||
|
Amount: "0",
|
||||||
|
Currency: "USD",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
userConn, userClose := newBufConnClient(t, func(server *grpc.Server) {
|
||||||
|
gatewaypb.RegisterChatAppUserServer(server, userServer)
|
||||||
|
})
|
||||||
|
payConn, payClose := newBufConnClient(t, func(server *grpc.Server) {
|
||||||
|
gatewaypb.RegisterChatAppPayServer(server, payServer)
|
||||||
|
})
|
||||||
|
|
||||||
|
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
|
||||||
|
authService := auth.New(usergrpc.New(gatewaypb.NewChatAppUserClient(userConn), userTimeout))
|
||||||
|
payService := pay.New(paygrpc.New(gatewaypb.NewChatAppPayClient(payConn), payTimeout))
|
||||||
|
readinessChecker := &mockReadinessChecker{}
|
||||||
|
handler := httpserver.New("chatappgateway", ":0", 2*time.Second, logger, authService, payService, readinessChecker).Handler()
|
||||||
|
|
||||||
|
return &testEnv{
|
||||||
|
server: httptest.NewServer(handler),
|
||||||
|
userServer: userServer,
|
||||||
|
payServer: payServer,
|
||||||
|
readinessChecker: readinessChecker,
|
||||||
|
closeFn: func() {
|
||||||
|
userClose()
|
||||||
|
payClose()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newBufConnClient(t *testing.T, register func(*grpc.Server)) (*grpc.ClientConn, func()) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
listener := bufconn.Listen(1024 * 1024)
|
||||||
|
server := grpc.NewServer()
|
||||||
|
register(server)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
_ = server.Serve(listener)
|
||||||
|
}()
|
||||||
|
|
||||||
|
conn, err := grpc.DialContext(
|
||||||
|
context.Background(),
|
||||||
|
"bufnet",
|
||||||
|
grpc.WithContextDialer(func(context.Context, string) (net.Conn, error) {
|
||||||
|
return listener.Dial()
|
||||||
|
}),
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DialContext returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn, func() {
|
||||||
|
_ = conn.Close()
|
||||||
|
server.Stop()
|
||||||
|
_ = listener.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func doRequest(t *testing.T, client *http.Client, method string, url string, body io.Reader) (*http.Response, []byte) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
request, err := http.NewRequestWithContext(context.Background(), method, url, body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewRequestWithContext returned error: %v", err)
|
||||||
|
}
|
||||||
|
if body != nil {
|
||||||
|
request.Header.Set("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := client.Do(request)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("client.Do returned error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ReadAll returned error: %v", err)
|
||||||
|
}
|
||||||
|
response.Body = io.NopCloser(bytes.NewReader(payload))
|
||||||
|
return response, payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertErrorResponse(t *testing.T, gotStatus int, body []byte, wantStatus int, wantMessage string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
if gotStatus != wantStatus {
|
||||||
|
t.Fatalf("unexpected status: got=%d want=%d body=%s", gotStatus, wantStatus, string(body))
|
||||||
|
}
|
||||||
|
|
||||||
|
var payload errorResponse
|
||||||
|
if err := json.Unmarshal(body, &payload); err != nil {
|
||||||
|
t.Fatalf("Unmarshal returned error: %v", err)
|
||||||
|
}
|
||||||
|
if payload.Message != wantMessage {
|
||||||
|
t.Fatalf("unexpected message: got=%q want=%q", payload.Message, wantMessage)
|
||||||
|
}
|
||||||
|
if payload.RequestID == "" {
|
||||||
|
t.Fatal("request_id should not be empty")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type successResponse[T any] struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
RequestID string `json:"request_id"`
|
||||||
|
Data T `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorResponse struct {
|
||||||
|
Code string `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
RequestID string `json:"request_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockUserServer struct {
|
||||||
|
gatewaypb.UnimplementedChatAppUserServer
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
callCount int
|
||||||
|
lastReq *gatewaypb.LoginRequest
|
||||||
|
response *gatewaypb.LoginResponse
|
||||||
|
err error
|
||||||
|
delay time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mockUserServer) Login(ctx context.Context, request *gatewaypb.LoginRequest) (*gatewaypb.LoginResponse, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
s.callCount++
|
||||||
|
copied := *request
|
||||||
|
s.lastReq = &copied
|
||||||
|
response := s.response
|
||||||
|
err := s.err
|
||||||
|
delay := s.delay
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
if delay > 0 {
|
||||||
|
select {
|
||||||
|
case <-time.After(delay):
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mockUserServer) CallCount() int {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
return s.callCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mockUserServer) LastRequest() *gatewaypb.LoginRequest {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if s.lastReq == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
copied := *s.lastReq
|
||||||
|
return &copied
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockPayServer struct {
|
||||||
|
gatewaypb.UnimplementedChatAppPayServer
|
||||||
|
|
||||||
|
mu sync.Mutex
|
||||||
|
lastReq *gatewaypb.QueryOrderRequest
|
||||||
|
response *gatewaypb.QueryOrderResponse
|
||||||
|
err error
|
||||||
|
delay time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mockPayServer) QueryOrder(ctx context.Context, request *gatewaypb.QueryOrderRequest) (*gatewaypb.QueryOrderResponse, error) {
|
||||||
|
s.mu.Lock()
|
||||||
|
copied := *request
|
||||||
|
s.lastReq = &copied
|
||||||
|
response := s.response
|
||||||
|
err := s.err
|
||||||
|
delay := s.delay
|
||||||
|
s.mu.Unlock()
|
||||||
|
|
||||||
|
if delay > 0 {
|
||||||
|
select {
|
||||||
|
case <-time.After(delay):
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return response, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mockPayServer) LastRequest() *gatewaypb.QueryOrderRequest {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
if s.lastReq == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
copied := *s.lastReq
|
||||||
|
return &copied
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockReadinessChecker struct {
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *mockReadinessChecker) Ready(context.Context) error {
|
||||||
|
return c.err
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user