Initial commit

This commit is contained in:
lizhenping 2025-09-09 09:27:43 +08:00
commit b0ebc38928
22 changed files with 673 additions and 0 deletions

37
.github/workflows/buildx.yaml vendored Normal file
View File

@ -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 }}

23
.gitignore vendored Normal file
View File

@ -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

118
README.MD Normal file
View File

@ -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")
```

4
config/api.yml Normal file
View File

@ -0,0 +1,4 @@
#渠道API信息
url: https://api.xxpay.com
key: xxpay_api_key
secret: xxpay_api_secret

12
config/config.toml Normal file
View File

@ -0,0 +1,12 @@
[app]
env="dev"
[grpc]
name="org"
address=":9901"
[logger]
level="all"
stdout=true

54
go.mod Normal file
View File

@ -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
)

20
internal/cmd/cmd.go Normal file
View File

@ -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
},
}
)

View File

@ -0,0 +1 @@
package consts

View File

@ -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)
}

41
internal/logic/demo.go Normal file
View File

@ -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)
}
}

34
internal/logic/event.go Normal file
View File

@ -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
}

View File

@ -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
}

21
internal/logic/pay.go Normal file
View File

@ -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
}

13
internal/logic/ping.go Normal file
View File

@ -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)
}

17
internal/logic/query.go Normal file
View File

@ -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)
}

13
internal/logic/refund.go Normal file
View File

@ -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)
}

13
internal/logic/revoke.go Normal file
View File

@ -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)
}

View File

@ -0,0 +1 @@
package packed

15
internal/service/grpc.go Normal file
View File

@ -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()
}

View File

@ -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
}

25
internal/service/state.go Normal file
View File

@ -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
}

11
main.go Normal file
View File

@ -0,0 +1,11 @@
package main
import (
"github.com/gogf/gf/v2/os/gctx"
"linkpay/internal/cmd"
)
func main() {
cmd.Main.Run(gctx.GetInitCtx())
}