commit b0ebc38928b5a323912cd7249f2f2732fa441601 Author: lizhenping <964294726@qq.com> Date: Tue Sep 9 09:27:43 2025 +0800 Initial commit diff --git a/.github/workflows/buildx.yaml b/.github/workflows/buildx.yaml new file mode 100644 index 0000000..111f19d --- /dev/null +++ b/.github/workflows/buildx.yaml @@ -0,0 +1,37 @@ +name: Buildx +on: + push: + branches: + - latest + tags: + - 'v*' + - 'release-*' + - 'preview-*' + +jobs: + Pipline: + runs-on: ubuntu-latest + steps: + - name: Get REPO NAME + run: echo "REPO_NAME=${GITHUB_REPOSITORY#*/}" >> $GITHUB_ENV + + - name: Checkout code + uses: https://git.linkiio.cn/actions/checkout@v3 + + - name: Login to Docker Registry + uses: https://git.linkiio.cn/actions/login-action@v2 + with: + registry: docker.linkiio.cn + username: ${{ vars.REGISTRY_USERNAME }} + password: ${{ vars.REGISTRY_PASSWORD }} + + - name: Build Push Docker imagee + uses: https://git.linkiio.cn/actions/build-push-action@v3 + with: + context: . + file: https://open.linkiio.cn/docker/linkpay + push: true + tags: docker.linkiio.cn/linkpay/${{ env.REPO_NAME }}:${{ github.ref_name }} + build-args: | + GOPRIVATE=${{ secrets.GOPRIVATE }} + NAME=${{ env.REPO_NAME }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..52c699e --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +!.gitignore +.buildpath +.hgignore.swp +.project +.orig +.swp +.idea/ +.settings/ +.vscode/ +bin/ +**/.DS_Store +gf +main +main.exe +hack* +manifest +output/ +temp/ +temp.yaml +bin +*.sum +**/config/config.yaml + diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..e41fba9 --- /dev/null +++ b/README.MD @@ -0,0 +1,118 @@ +# Linkpay Grpc Service + + +## Introduction + +This is the grpc service for Linkpay. +## Directory Structure +``` +├── api //api预留 +├── config //配置文件 +│ └── config.toml +├── internal //应用实现 +│ ├── cmd +│ │ └── cmd.go +│ ├── consts //常量/结构定义 +│ │ └── consts.go +│ ├── controller //控制层 +│ │ └── api +│ │ └── api.go +│ ├── logic //逻辑层(主要业务代码实现) +│ │ ├── event.go //回调逻辑实现 +│ │ ├── pay.go //支付逻辑实现 +│ │ ├── ping.go //碰撞测试 +│ │ ├── query.go //查询逻辑实现 +│ │ └── refund.go //退款逻辑实现 +│ │ └── revoke.go //撤销逻辑实现 +│ ├── packed //三方包 +│ │ ├── packed.go +│ │ └── unarymeta.go +│ └── service //服务层 +│ ├── grpc.go +│ └── request.go +├── main.go //入口 +``` + +### 启动项目 +``` +go get -u && go run . +``` + +### 调试说明 + + + +下载postman工具: https://www.postman.com/downloads/ + +postman调试操作: +- 新建grpc接口。 +- 在url中输入:127.0.0.1:9901 即可 +- 在logic逻辑层实现各接口能力。 + +### 接口要求实现 +- 回调 event 需要返回回调结果 +- 碰撞测试 ping 需要返回pong/或当前时间戳 +- 创建支付 pay 需要返回支付订单信息 +- 查询支付 query 需要返回支付/退单的订单的状态信息 +- 退款 refund 需要返回退款订单信息 +- 撤销 revoke 需要返回撤销订单信息 + +### 回调说明 +- 回调接口需要返回回调结果,包括成功或失败,及相关信息。 +- 入口数据, req *protobuf.CallbackReq, 其中包含第三方的URL、Header、Body等信息。 + +- 返回数据, res *protobuf.Orders, 其中包含订单状态、订单号、订单金额等信息。尽可能的满足字段信息都需要返回。 +其中Response字段为回调接口返回的原始数据,方便业务层处理日志。 + + +```go +func Callback(ctx context.Context, req *protobuf.CallbackReq) (res *protobuf.Orders, err error) { + + res = &protobuf.Orders{ + Kid: req.Kid, //商户ID + Uid: req.Uid, //用户ID + Org: req.Org, //商户名称 + Via: req.Via, //支付渠道 + OrgNo: "12345", //渠道订单号 + OrderNo: "67890", //商户订单号 + Response: string(req.GetBody()), //解签后的Body数据 + State: -1, //订单状态 (参考 protobuf.State 枚举) + StateText: "支付失败", //订单状态原因,状态说明 + } + + return +} +``` + +### 通用API请求方法 +service/request.go + +```go +//请求API +r, err := service.Request(ctx).Header("Authorization", "{Token}").Post("/v1/pay/create", req) + +//返回支付结果 +res = &protobuf.Orders{ + Response: r.ReadAllString(), +} +``` + +该方法封装了请求的通用方法,包括: +- Header:设置请求头 +- Post:发起post请求 +- Get:发起get请求 + +其中多个Header头示例 +```go +r, err := service.Request(ctx).Header("x-api-key", "{Token}").Header("x-api-secret", "{Secret}").Post("/v1/pay/create", req) +``` + +r 方便获取各类HTTP响应信息,数据。 + +### 日志说明 +请严格遵守Goframe日志规范打印 +如: +```go +g.Log("业务模块").Info(ctx, "this is a info log") +g.Log("pay").Error(ctx, "this is a pay error log") +``` \ No newline at end of file diff --git a/config/api.yml b/config/api.yml new file mode 100644 index 0000000..a3f569a --- /dev/null +++ b/config/api.yml @@ -0,0 +1,4 @@ +#渠道API信息 +url: https://api.xxpay.com +key: xxpay_api_key +secret: xxpay_api_secret \ No newline at end of file diff --git a/config/config.toml b/config/config.toml new file mode 100644 index 0000000..0d9afc3 --- /dev/null +++ b/config/config.toml @@ -0,0 +1,12 @@ +[app] +env="dev" + +[grpc] +name="org" +address=":9901" + + +[logger] +level="all" +stdout=true + diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ebcd9fa --- /dev/null +++ b/go.mod @@ -0,0 +1,54 @@ +module linkpay + +go 1.23.0 + +require ( + git.linkiio.cn/linkpay/invoke v1.4.4 + git.linkiio.cn/linkpay/protobuf v1.5.3 + github.com/gogf/gf/contrib/rpc/grpcx/v2 v2.9.0 + github.com/gogf/gf/v2 v2.9.0 + google.golang.org/grpc v1.74.2 +) + +require ( + github.com/BurntSushi/toml v1.5.0 // indirect + github.com/clbanning/mxj/v2 v2.7.0 // indirect + github.com/coreos/go-semver v0.3.1 // indirect + github.com/coreos/go-systemd/v22 v22.5.0 // indirect + github.com/emirpasic/gods v1.18.1 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/gogf/gf/contrib/registry/etcd/v2 v2.9.0 // indirect + github.com/gogf/gf/contrib/registry/file/v2 v2.9.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/grokify/html-strip-tags-go v0.1.0 // indirect + github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.1 // indirect + github.com/magiconair/properties v1.8.10 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + go.etcd.io/etcd/api/v3 v3.6.4 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.4 // indirect + go.etcd.io/etcd/client/v3 v3.6.4 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/otel v1.37.0 // indirect + go.opentelemetry.io/otel/metric v1.37.0 // indirect + go.opentelemetry.io/otel/sdk v1.37.0 // indirect + go.opentelemetry.io/otel/trace v1.37.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap v1.27.0 // indirect + golang.org/x/net v0.42.0 // indirect + golang.org/x/sys v0.34.0 // indirect + golang.org/x/text v0.27.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20250804133106-a7a43d27e69b // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect + google.golang.org/protobuf v1.36.6 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/internal/cmd/cmd.go b/internal/cmd/cmd.go new file mode 100644 index 0000000..7c2c691 --- /dev/null +++ b/internal/cmd/cmd.go @@ -0,0 +1,20 @@ +package cmd + +import ( + "context" + "linkpay/internal/controller/api" + + "git.linkiio.cn/linkpay/invoke/grpcx" + + "github.com/gogf/gf/v2/os/gcmd" +) + +var ( + Main = gcmd.Command{ + Func: func(ctx context.Context, parser *gcmd.Parser) (err error) { + s := grpcx.Registry(api.Register) + s.Run() + return nil + }, + } +) diff --git a/internal/consts/consts.go b/internal/consts/consts.go new file mode 100644 index 0000000..d709a2b --- /dev/null +++ b/internal/consts/consts.go @@ -0,0 +1 @@ +package consts diff --git a/internal/controller/api/api.go b/internal/controller/api/api.go new file mode 100644 index 0000000..057c2f8 --- /dev/null +++ b/internal/controller/api/api.go @@ -0,0 +1,45 @@ +package api + +import ( + "context" + "linkpay/internal/logic" + + "git.linkiio.cn/linkpay/protobuf" + "github.com/gogf/gf/contrib/rpc/grpcx/v2" +) + +type Controller struct { + protobuf.UnimplementedOrgServer +} + +func Register(s *grpcx.GrpcServer) { + protobuf.RegisterOrgServer(s.Server, &Controller{}) +} + +func (*Controller) Event(ctx context.Context, req *protobuf.EventReq) (res *protobuf.EventRes, err error) { + return logic.Event(ctx, req) +} + +func (*Controller) Ping(ctx context.Context, req *protobuf.PingReq) (res *protobuf.PongRes, err error) { + return logic.Ping(ctx, req) +} + +func (*Controller) Pay(ctx context.Context, req *protobuf.PayReq) (res *protobuf.Orders, err error) { + return logic.Pay(ctx, req) +} + +func (*Controller) Refund(ctx context.Context, req *protobuf.RefundReq) (res *protobuf.Refunds, err error) { + return logic.Refund(ctx, req) +} + +func (*Controller) Revoke(ctx context.Context, req *protobuf.RefundReq) (res *protobuf.Refunds, err error) { + return logic.Revoke(ctx, req) +} + +func (*Controller) QueryOrder(ctx context.Context, req *protobuf.QueryReq) (res *protobuf.Orders, err error) { + return logic.QueryOrder(ctx, req) +} + +func (*Controller) QueryRefund(ctx context.Context, req *protobuf.QueryReq) (res *protobuf.Refunds, err error) { + return logic.QueryRefund(ctx, req) +} diff --git a/internal/logic/demo.go b/internal/logic/demo.go new file mode 100644 index 0000000..67321cb --- /dev/null +++ b/internal/logic/demo.go @@ -0,0 +1,41 @@ +package logic + +import ( + "context" + "linkpay/internal/service" + "log" + + "git.linkiio.cn/linkpay/protobuf" +) + +/* +* + - 双向通信流示例查询订单 + - @param void + - @author dc.To + - @version 20250429 + - ApiOrders(ctx, &protobuf.Orders{ + Org: "org", + }) +*/ +func ApiOrders(ctx context.Context, req *protobuf.Orders) { + + client := service.BssClient() + + stream, _ := client.Order(context.Background()) + + //查询协程 + go func() { + stream.Send(req) + stream.CloseSend() + }() + + //监听返回 + for { + res, err := stream.Recv() + if err != nil { + break + } + log.Printf("Got order response: %v", res) + } +} diff --git a/internal/logic/event.go b/internal/logic/event.go new file mode 100644 index 0000000..fd6dc5d --- /dev/null +++ b/internal/logic/event.go @@ -0,0 +1,34 @@ +package logic + +import ( + "context" + "linkpay/internal/service" + + "git.linkiio.cn/linkpay/protobuf" +) + +/** + * 回调事件实现 + * @author dc.To + * @version 20250418 + */ +func Event(ctx context.Context, req *protobuf.EventReq) (res *protobuf.EventRes, err error) { + + res = &protobuf.EventRes{ + Type: "order", + Data: &protobuf.EventRes_Order{ + Order: &protobuf.Orders{ + Kid: req.Kid, //商户ID + Uid: req.Uid, //用户ID + Org: req.Org, //商户名称 + Via: req.Via, //支付渠道 + OrgNo: "12345", //渠道订单号 + OrderNo: "67890", //商户订单号 + Response: string(req.GetBody()), //解签后的Body数据 + State: int32(service.GetState("error")), //订单状态 (参考 protobuf.State 枚举 @see https://git.linkiio.cn/linkpay/protobuf/src/commit/982698cc1cf9d050d21f904563b5403d1f11d987/org.pb.go#L26) + StateText: "支付失败", //渠道方状态原因,状态说明 + }, + }, + } + return +} diff --git a/internal/logic/event_test.go b/internal/logic/event_test.go new file mode 100644 index 0000000..dc92062 --- /dev/null +++ b/internal/logic/event_test.go @@ -0,0 +1,64 @@ +package logic + +import ( + "context" + "linkpay/internal/service" + + "git.linkiio.cn/linkpay/protobuf" + "github.com/gogf/gf/v2/encoding/gbase64" + "github.com/gogf/gf/v2/encoding/gjson" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" + "github.com/gogf/gf/v2/frame/g" + "google.golang.org/grpc/metadata" +) + +func Config(ctx context.Context) *gjson.Json { + md, ok := metadata.FromIncomingContext(ctx) + + if !ok { + return &gjson.Json{} + } + + c := md.Get("x-cfg-api") + b, _ := gbase64.DecodeString(c[0]) + return gjson.New(b) + +} + +/** + * 回调事件实现 + * @author dc.To + * @version 20250418 + */ +func EventTestCase(ctx context.Context, req *protobuf.EventReq) (res *protobuf.EventRes, err error) { + + if req.Kid != Config(ctx).Get("appId").Uint64() || req.Kid != g.Cfg("api").MustGet(ctx, "appId").Uint64() { + g.Log().Errorf(ctx, "req-Kid: %d == grpc-Kid: %d == cfg-Kid: %d", req.Kid, Config(ctx).Get("appId").Uint64(), g.Cfg("api").MustGet(ctx, "appId").Uint64()) + return nil, gerror.NewCodef(gcode.CodeInternalError, "req-Kid [%d] not match Grpc Kid [%d] or cfg-Kid [%d]", req.Kid, Config(ctx).Get("appId").Uint64(), g.Cfg("api").MustGet(ctx, "appId").Uint64()) + } else { + g.Log().Infof(ctx, "req-Kid: %d == grpc-Kid: %d == cfg-Kid: %d", req.Kid, Config(ctx).Get("appId").Uint64(), g.Cfg("api").MustGet(ctx, "appId").Uint64()) + } + + return &protobuf.EventRes{ + Body: Config(ctx).Get("appId").String(), + }, nil + + res = &protobuf.EventRes{ + Type: "order", + Data: &protobuf.EventRes_Order{ + Order: &protobuf.Orders{ + Kid: req.Kid, //商户ID + Uid: req.Uid, //用户ID + Org: req.Org, //商户名称 + Via: req.Via, //支付渠道 + OrgNo: "12345", //渠道订单号 + OrderNo: "67890", //商户订单号 + Response: string(req.GetBody()), //解签后的Body数据 + State: int32(service.GetState("error")), //订单状态 (参考 protobuf.State 枚举 @see https://git.linkiio.cn/linkpay/protobuf/src/commit/982698cc1cf9d050d21f904563b5403d1f11d987/org.pb.go#L26) + StateText: "支付失败", //渠道方状态原因,状态说明 + }, + }, + } + return +} diff --git a/internal/logic/pay.go b/internal/logic/pay.go new file mode 100644 index 0000000..36c2a2b --- /dev/null +++ b/internal/logic/pay.go @@ -0,0 +1,21 @@ +package logic + +import ( + "context" + "linkpay/internal/service" + + "git.linkiio.cn/linkpay/protobuf" +) + +func Pay(ctx context.Context, req *protobuf.PayReq) (res *protobuf.Orders, err error) { + + //测试请求API + r, err := service.Request(ctx).Header("Authorization", "{Token}").ContentType("application/json").Post("", req) + + //返回支付结果 + res = &protobuf.Orders{ + Response: r.ReadAllString(), + } + + return +} diff --git a/internal/logic/ping.go b/internal/logic/ping.go new file mode 100644 index 0000000..9bee21d --- /dev/null +++ b/internal/logic/ping.go @@ -0,0 +1,13 @@ +package logic + +import ( + "context" + + "git.linkiio.cn/linkpay/protobuf" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" +) + +func Ping(ctx context.Context, req *protobuf.PingReq) (res *protobuf.PongRes, err error) { + return nil, gerror.NewCode(gcode.CodeNotImplemented) +} diff --git a/internal/logic/query.go b/internal/logic/query.go new file mode 100644 index 0000000..6e3efdc --- /dev/null +++ b/internal/logic/query.go @@ -0,0 +1,17 @@ +package logic + +import ( + "context" + + "git.linkiio.cn/linkpay/protobuf" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" +) + +func QueryOrder(ctx context.Context, req *protobuf.QueryReq) (res *protobuf.Orders, err error) { + return nil, gerror.NewCode(gcode.CodeNotImplemented) +} + +func QueryRefund(ctx context.Context, req *protobuf.QueryReq) (res *protobuf.Refunds, err error) { + return nil, gerror.NewCode(gcode.CodeNotImplemented) +} diff --git a/internal/logic/refund.go b/internal/logic/refund.go new file mode 100644 index 0000000..67da5e0 --- /dev/null +++ b/internal/logic/refund.go @@ -0,0 +1,13 @@ +package logic + +import ( + "context" + + "git.linkiio.cn/linkpay/protobuf" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" +) + +func Refund(ctx context.Context, req *protobuf.RefundReq) (res *protobuf.Refunds, err error) { + return nil, gerror.NewCode(gcode.CodeNotImplemented) +} diff --git a/internal/logic/revoke.go b/internal/logic/revoke.go new file mode 100644 index 0000000..8e410b1 --- /dev/null +++ b/internal/logic/revoke.go @@ -0,0 +1,13 @@ +package logic + +import ( + "context" + + "git.linkiio.cn/linkpay/protobuf" + "github.com/gogf/gf/v2/errors/gcode" + "github.com/gogf/gf/v2/errors/gerror" +) + +func Revoke(ctx context.Context, req *protobuf.RefundReq) (res *protobuf.Refunds, err error) { + return nil, gerror.NewCode(gcode.CodeNotImplemented) +} diff --git a/internal/packed/packed.go b/internal/packed/packed.go new file mode 100644 index 0000000..e20ab1e --- /dev/null +++ b/internal/packed/packed.go @@ -0,0 +1 @@ +package packed diff --git a/internal/service/grpc.go b/internal/service/grpc.go new file mode 100644 index 0000000..da57c0a --- /dev/null +++ b/internal/service/grpc.go @@ -0,0 +1,15 @@ +package service + +import ( + "git.linkiio.cn/linkpay/protobuf" + "git.linkiio.cn/linkpay/invoke/grpcx" +) + +/** + * BSS 双向通信流 + * @author dc.To + * @version 20250410 + */ +func BssClient() protobuf.BssClient { + return grpcx.BssClient() +} diff --git a/internal/service/request.go b/internal/service/request.go new file mode 100644 index 0000000..da60665 --- /dev/null +++ b/internal/service/request.go @@ -0,0 +1,91 @@ +package service + +import ( + "context" + "net/http" + "os" + + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/net/gclient" +) + +type RequestContext struct { + *gclient.Client + context.Context +} + +/** + * 统一API请求方法 + * @param void + * @author dc.To + * @version 20250313 + */ +func Request(ctx context.Context) *RequestContext { + + c := &RequestContext{ + Client: gclient.New(), + Context: ctx, + } + + url, err := g.Cfg("api").Get(ctx, "url") + if err != nil { + g.Log().Error(ctx, "Api url not found in config file: ", err.Error()) + } + + c.SetPrefix(url.String()) + c.SetContentType("application/json") + c.Use(handle) + return c +} + +func (r *RequestContext) Header(k, v string) *RequestContext { + r.SetHeader(k, v) + return r +} + +func (r *RequestContext) ContentType(t string) *RequestContext { + r.SetContentType(t) + return r +} + +func (r *RequestContext) Get(path string, data interface{}) (*gclient.Response, error) { + return r.Client.Get(r.Context, path, data) +} + +func (r *RequestContext) Post(path string, data interface{}) (*gclient.Response, error) { + return r.Client.Post(r.Context, path, data) +} + +func (r *RequestContext) GetVar(path string, data interface{}) *g.Var { + return r.Client.GetVar(r.Context, path, data) +} + +func (r *RequestContext) PostVar(path string, data interface{}) *g.Var { + return r.Client.PostVar(r.Context, path, data) +} + +func (r *RequestContext) GetBytes(path string, data interface{}) []byte { + return r.Client.GetBytes(r.Context, path, data) +} + +func (r *RequestContext) PostBytes(path string, data interface{}) []byte { + return r.Client.PostBytes(r.Context, path, data) +} + +func (r *RequestContext) GetContent(path string, data interface{}) string { + return r.Client.GetContent(r.Context, path, data) +} + +func (r *RequestContext) PostContent(path string, data interface{}) string { + return r.Client.PostContent(r.Context, path, data) +} + +func handle(c *gclient.Client, r *http.Request) (resp *gclient.Response, err error) { + resp, err = c.Next(r) + + if os.Getenv("APP_ENV") == "dev" { + resp.RawDump() + } + + return resp, err +} diff --git a/internal/service/state.go b/internal/service/state.go new file mode 100644 index 0000000..c53d23a --- /dev/null +++ b/internal/service/state.go @@ -0,0 +1,25 @@ +package service + +import "git.linkiio.cn/linkpay/protobuf" + +/** + * 返回状态 参考 protobuf.State 枚举 + * 本方法做为示例,实际业务中,根据渠道返回状态时,需要根据具体情况进行判断和转换 + * @see https://git.linkiio.cn/linkpay/protobuf/src/commit/982698cc1cf9d050d21f904563b5403d1f11d987/org.pb.go#L26 + * @param void + * @author dc.To + * @version 20250425 + */ +func GetState(orgState interface{}) protobuf.State { + + var state = protobuf.State_Pending + + //当渠道终态 成功时,状态为成功 + if orgState == "success" { + state = protobuf.State_Success + } else if orgState == "failed" { + state = protobuf.State_Failure + } + + return state +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..2e2a8a6 --- /dev/null +++ b/main.go @@ -0,0 +1,11 @@ +package main + +import ( + "github.com/gogf/gf/v2/os/gctx" + + "linkpay/internal/cmd" +) + +func main() { + cmd.Main.Run(gctx.GetInitCtx()) +}