go项目适配DTM,gozero已经适配dtm了,goframe项目要怎么适配?

所谓的gozero适配dtm

以下是gozero的示例,本质不就是维护了dtm配置

go-zero支持

dtm与go-zero进行了深度合作,打造了go-zero原生支持分布式事务的解决方案,提供了极简的用户体验。

dtm从v1.6.0开始原生支持go-zero微服务框架,go-zero的版本需要v1.2.4以上

运行一个已有的示例

我们以etcd作为注册服务中心为例,按照如下步骤运行一个go-zero的示例:

  • 启动etcd
bash 复制代码
# 前提:已安装etcd
etcd
  • 配置dtm
yaml 复制代码
MicroService:
  Driver: 'dtm-driver-gozero' # 配置dtm使用go-zero的微服务协议
  Target: 'etcd://localhost:2379/dtmservice' # 把dtm注册到etcd的这个地址
  EndPoint: 'localhost:36790' # dtm的本地地址
  • 启动dtm
bash 复制代码
# 前提:配置好conf.yml
go run app/main.go -c conf.yml
  • 运行一个go-zero的服务
bash 复制代码
git clone https://github.com/dtm-labs/dtmdriver-clients && cd dtmdriver-clients
cd gozero/trans && go run trans.go
  • 发起一个go-zero使用dtm的事务
bash 复制代码
# 在dtmdriver-clients的目录下
cd gozero/app && go run main.go

当您在trans的日志中看到

csharp 复制代码
2021/12/03 15:44:05 transfer out 30 cents from 1
2021/12/03 15:44:05 transfer in 30 cents to 2
2021/12/03 15:44:05 transfer out 30 cents from 1
2021/12/03 15:44:05 transfer out 30 cents from 1

那就是事务正常完成了

开发接入

go 复制代码
// 下面这行导入gozero的dtm驱动
import _ "github.com/dtm-labs/driver-gozero"

// dtm已经通过前面的配置,注册到下面这个地址,因此在dtmgrpc中使用该地址
var dtmServer = "etcd://localhost:2379/dtmservice"

// 下面从配置文件中Load配置,然后通过BuildTarget获得业务服务的地址
var c zrpc.RpcClientConf
conf.MustLoad(*configFile, &c)
busiServer, err := c.BuildTarget()

  // 使用dtmgrpc生成一个消息型分布式事务并提交
	gid := dtmgrpc.MustGenGid(dtmServer)
	msg := dtmgrpc.NewMsgGrpc(dtmServer, gid).
    // 事务的第一步为调用trans.TransSvcClient.TransOut
    // 可以从trans.pb.go中找到上述方法对应的Method名称为"/trans.TransSvc/TransOut"
    // dtm需要从dtm服务器调用该方法,所以不走强类型
    // 而是走动态的url: busiServer+"/trans.TransSvc/TransOut"
		Add(busiServer+"/trans.TransSvc/TransOut", &busi.BusiReq{Amount: 30, UserId: 1}).
		Add(busiServer+"/trans.TransSvc/TransIn", &busi.BusiReq{Amount: 30, UserId: 2})
	err := msg.Submit()

整个开发接入的过程很少,前面的注释已经很清晰,就不再赘述了

正确的做法,任何go项目都可以去适配dtm

说白了就是让服务找到dtm的调用入口

1. 使用dtm,传参dtm server地址

go 复制代码
gid := dtmgrpc.MustGenGid(l.svcCtx.Config.DtmServerAddr)

2. tcc事务上下文丢失解决方案

go 复制代码
package dtmtool

import (
	"context"
	"violet/common"

	"github.com/dtm-labs/client/dtmcli/dtmimp"
	"github.com/dtm-labs/client/dtmgrpc"
	"github.com/dtm-labs/client/dtmgrpc/dtmgimp"
	"go.opentelemetry.io/otel"
)

func TccGlobalTransactionWithContext(ctx context.Context, dtm string, gid string, tccFunc dtmgrpc.TccGlobalFunc) (rerr error) {
	headers := make(common.BranchHeaders)
	propagator := otel.GetTextMapPropagator()
	propagator.Inject(ctx, headers)

	tcc := &dtmgrpc.TccGrpc{TransBase: *dtmimp.NewTransBase(gid, "tcc", dtm, "")}

	tcc.Context = ctx
	tcc.BranchHeaders = headers

	rerr = dtmgimp.DtmGrpcCall(&tcc.TransBase, "Prepare")
	if rerr != nil {
		return rerr
	}
	defer dtmimp.DeferDo(&rerr, func() error {
		return dtmgimp.DtmGrpcCall(&tcc.TransBase, "Submit")
	}, func() error {
		tcc.RollbackReason = rerr.Error()
		return dtmgimp.DtmGrpcCall(&tcc.TransBase, "Abort")
	})
	rerr = tccFunc(tcc)
	return
}

func SagaTransactionWithContext(ctx context.Context, dtm string, gid string) *dtmgrpc.SagaGrpc {
	headers := make(common.BranchHeaders)
	propagator := otel.GetTextMapPropagator()
	propagator.Inject(ctx, headers)

	saga := dtmgrpc.NewSagaGrpc(dtm, gid)

	saga.Context = ctx
	saga.BranchHeaders = headers

	return saga
}

3. 完整示例

go 复制代码
gid := dtmgrpc.MustGenGid(l.svcCtx.Config.DtmServerAddr)
	err = dtmtool.TccGlobalTransactionWithContext(l.ctx, l.svcCtx.Config.DtmServerAddr, gid, func(tcc *dtmgrpc.TccGrpc) error {
		financeRpcFullAddr := k8stool.CombineFullAddr(l.svcCtx.Config.Namespace, l.svcCtx.Config.FinanceRpcAddr)
		userRpcFullAddr := k8stool.CombineFullAddr(l.svcCtx.Config.Namespace, l.svcCtx.Config.UserRpcAddr)

		transferRequest := &financeRpc.CreateTransferApplyReq{
			UserId:      userId,
			UserName:    userName,
			OutPlatform: req.FromAccountPlatform,
			InPlatform:  req.ToAccountPlatform,
			OutAmount:   amount.String(),
			InAmount:    inAmount,
			OutCurrency: fromCurrency,
			InCurrency:  toCurrency,
			OutAccount:  req.FromAccount,
			InAccount:   req.ToAccount,
			OrderSource: orderSource,
			Rate:        rate,
		}
		var transferResp financeRpc.CreateTransferApplyRes
		err = tcc.CallBranch(transferRequest,
			financeRpcFullAddr+financeRpc.TransferApplication_TccTryCreateTransferApply_FullMethodName,
			financeRpcFullAddr+financeRpc.TransferApplication_TccConfirmCreateTransferApply_FullMethodName,
			financeRpcFullAddr+financeRpc.TransferApplication_TccCancelCreateTransferApply_FullMethodName,
			&transferResp)
		if err != nil {
			logx.WithContext(l.ctx).Errorf("划转记录失败,err:%v", err)
			return err
		}
		if transferResp.Status.Code != 0 {
			return common.NewErr(int(transferResp.Status.Code), transferResp.Status.Message)
		}

		resp = &types.TransactionResponse{
			Amount:         amount.String(),
			AmountReceived: inAmount,
			OrderId:        transferResp.OrderId,
			OrderTime:      transferResp.CreateTime,
		}

		if req.FromAccountPlatform == comment.AccountWallet {
			// 钱包调整记录
			walletAdjustmentReq := &walletAdjust.CreateWalletAdjustmentReq{
				UserId:             userId,
				UserName:           userName,
				UserType:           userType,
				ChangeBeforeAmount: fromBalance.String(),
				AdminUser:          comment.AdminUser,
				ChangeType:         int32(comment.OUT_Amount),
				Wallet:             req.FromAccount,
				WalletCurrency:     fromCurrency,
				ChangeAmount:       amount.String(),
				ChangeMethod:       int32(comment.INNER_TRANS),
				TradeOrder:         &transferResp.OrderId,
			}
			var walletAdjustmentRes walletAdjust.Status
			err = tcc.CallBranch(walletAdjustmentReq,
				financeRpcFullAddr+financeRpc.WalletAdjustment_TccTryCreateWalletAdjustment_FullMethodName,
				financeRpcFullAddr+financeRpc.WalletAdjustment_TccConfirmCreateWalletAdjustment_FullMethodName,
				financeRpcFullAddr+financeRpc.WalletAdjustment_TccCancelCreateWalletAdjustment_FullMethodName,
				&walletAdjustmentRes)
			if err != nil {
				logx.WithContext(l.ctx).Errorf("钱包调整记录失败,err:%v", err)
				return err
			}
			if walletAdjustmentRes.Status.Code != 0 {
				return common.NewErr(int(walletAdjustmentRes.Status.Code), walletAdjustmentRes.Status.Message)
			}
			// 钱包流水
			walletTransHisReq := &walletAdjust.WalletTransactionHistory{
				UserId:             userId,
				UserName:           userName,
				UserType:           userType,
				WalletId:           req.FromAccount,
				Currency:           fromCurrency,
				Amount:             req.Amount,
				ChangeBeforeAmount: fromBalance.String(),
				ChangeType:         int32(comment.OUT_Amount),
				Remark:             "划转,转出到交易账户",
				ChangeMethod:       common.TransactionTypeTransferOut,
				OrderId:            transferResp.OrderId,
			}
			var walletTransHisRes walletAdjust.WalletTransactionHistoryID
			err = tcc.CallBranch(walletTransHisReq,
				financeRpcFullAddr+financeRpc.WalletAdjustment_TccTryCreateWalletTransactionHistory_FullMethodName,
				financeRpcFullAddr+financeRpc.WalletAdjustment_TccConfirmCreateWalletTransactionHistory_FullMethodName,
				financeRpcFullAddr+financeRpc.WalletAdjustment_TccCancelCreateWalletTransactionHistory_FullMethodName,
				&walletTransHisRes)
			if err != nil {
				logx.WithContext(l.ctx).Errorf("tcc write wallet transaction history error: %v", err)
				return err
			}
			if walletTransHisRes.Status.Code != 0 {
				return common.NewErr(int(walletTransHisRes.Status.Code), walletTransHisRes.Status.Message)
			}
			// 钱包减钱
			var adjustWalletBalance userRpc.Status
			err = tcc.CallBranch(&userRpc.RWalletBalanceAdjustRequest{
				WalletId: req.FromAccount,
				Type:     2,
				Amount:   amount.String(),
			},
				userRpcFullAddr+userRpc.RWallet_TccTryAdjustRWalletBalance_FullMethodName,
				userRpcFullAddr+userRpc.RWallet_TccConfirmAdjustRWalletBalance_FullMethodName,
				userRpcFullAddr+userRpc.RWallet_TccCancelCreateRWallet_FullMethodName,
				&adjustWalletBalance)
			if err != nil {
				logx.WithContext(l.ctx).Errorf("钱包余额调整失败,err:%v", err)
				return err
			}
			if adjustWalletBalance.Status.Code != 0 {
				return common.NewErr(int(adjustWalletBalance.Status.Code), adjustWalletBalance.Status.Message)
			}
			// 账户调整记录
			accountAdjustmentReq := &accountAdjust.CreateAccountAdjustmentReq{
				UserId:          userId,
				UserName:        userName,
				UserType:        userType,
				ChangeType:      int32(comment.IN_Amount),
				AdminUser:       comment.AdminUser,
				Account:         req.ToAccount,
				AccountPlatform: req.ToAccountPlatform,
				Currency:        toCurrency,
				BeforeAmount:    toBalance.String(),
				ChangeAmount:    inAmount,
				ChangeMethod:    int32(comment.INNER_TRANS),
				TradeOrder:      &transferResp.OrderId,
			}

			var accountAdjustmentRes accountAdjust.CreateAccountAdjustmentRes
			err = tcc.CallBranch(accountAdjustmentReq,
				financeRpcFullAddr+financeRpc.AccountAdjustment_TccTryCreateAccountAdjustment_FullMethodName,
				financeRpcFullAddr+financeRpc.AccountAdjustment_TccConfirmCreateAccountAdjustment_FullMethodName,
				financeRpcFullAddr+financeRpc.AccountAdjustment_TccCancelCreateAccountAdjustment_FullMethodName,
				&accountAdjustmentRes)
			if err != nil {
				logx.WithContext(l.ctx).Errorf("账户调整记录失败,err:%v", err)
				return err
			}
			if accountAdjustmentRes.Status.Code != 0 {
				return common.NewErr(int(accountAdjustmentRes.Status.Code), accountAdjustmentRes.Status.Message)
			}
			// 账户加钱
			payload := &finance_task.AccountAdjustBalance{
				ApplyId:      transferResp.OrderId,
				AdjustmentID: accountAdjustmentRes.OrderId,
			}
			taskInfo, _ := l.svcCtx.StaskClient.EnqueueContext(l.ctx, payload.CreateTransactionTaskFactory())
			logx.WithContext(l.ctx).Infof("task id: %+v", taskInfo.ID)
		} else {
			// 钱包调整记录
			walletAdjustmentReq := &walletAdjust.CreateWalletAdjustmentReq{
				UserId:             userId,
				UserName:           userName,
				UserType:           userType,
				ChangeBeforeAmount: toBalance.String(),
				AdminUser:          comment.AdminUser,
				ChangeType:         int32(comment.IN_Amount),
				Wallet:             req.ToAccount,
				WalletCurrency:     toCurrency,
				ChangeAmount:       inAmount,
				ChangeMethod:       int32(comment.INNER_TRANS),
				TradeOrder:         &transferResp.OrderId,
			}
			var walletAdjustmentRes walletAdjust.Status
			err = tcc.CallBranch(walletAdjustmentReq,
				financeRpcFullAddr+financeRpc.WalletAdjustment_TccTryCreateWalletAdjustment_FullMethodName,
				financeRpcFullAddr+financeRpc.WalletAdjustment_TccConfirmCreateWalletAdjustment_FullMethodName,
				financeRpcFullAddr+financeRpc.WalletAdjustment_TccCancelCreateWalletAdjustment_FullMethodName,
				&walletAdjustmentRes)
			if err != nil {
				logx.WithContext(l.ctx).Errorf("钱包调整记录失败,err:%v", err)
				return err
			}
			if walletAdjustmentRes.Status.Code != 0 {
				return common.NewErr(int(walletAdjustmentRes.Status.Code), walletAdjustmentRes.Status.Message)
			}
			// 钱包流水
			walletTransHisReq := &walletAdjust.WalletTransactionHistory{
				OrderId:            transferResp.OrderId,
				UserId:             userId,
				UserName:           userName,
				UserType:           userType,
				WalletId:           req.ToAccount,
				Currency:           toCurrency,
				Amount:             inAmount,
				ChangeBeforeAmount: toBalance.String(),
				ChangeType:         int32(comment.IN_Amount),
				Remark:             "划转,交易账户转入",
				ChangeMethod:       common.TransactionTypeTransferIn,
			}
			var walletTransHisRes walletAdjust.WalletTransactionHistoryID
			err = tcc.CallBranch(walletTransHisReq,
				financeRpcFullAddr+financeRpc.WalletAdjustment_TccTryCreateWalletTransactionHistory_FullMethodName,
				financeRpcFullAddr+financeRpc.WalletAdjustment_TccConfirmCreateWalletTransactionHistory_FullMethodName,
				financeRpcFullAddr+financeRpc.WalletAdjustment_TccCancelCreateWalletTransactionHistory_FullMethodName,
				&walletTransHisRes)
			if err != nil {
				logx.WithContext(l.ctx).Errorf("tcc write wallet transaction history error: %v", err)
				return err
			}
			if walletTransHisRes.Status.Code != 0 {
				return common.NewErr(int(walletTransHisRes.Status.Code), walletTransHisRes.Status.Message)
			}
			// 钱包加钱
			var adjustWalletBalance userRpc.Status
			err = tcc.CallBranch(&userRpc.RWalletBalanceAdjustRequest{
				WalletId: req.ToAccount,
				Type:     1,
				Amount:   inAmount,
			},
				userRpcFullAddr+userRpc.RWallet_TccTryAdjustRWalletBalance_FullMethodName,
				userRpcFullAddr+userRpc.RWallet_TccConfirmAdjustRWalletBalance_FullMethodName,
				userRpcFullAddr+userRpc.RWallet_TccCancelCreateRWallet_FullMethodName,
				&adjustWalletBalance)
			if err != nil {
				logx.WithContext(l.ctx).Errorf("钱包余额调整失败,err:%v", err)
				return err
			}
			if adjustWalletBalance.Status.Code != 0 {
				return common.NewErr(int(adjustWalletBalance.Status.Code), adjustWalletBalance.Status.Message)
			}

			// 账户调整记录
			accountAdjustmentReq := &accountAdjust.CreateAccountAdjustmentReq{
				UserId:          userId,
				UserName:        userName,
				UserType:        userType,
				ChangeType:      int32(comment.OUT_Amount),
				AdminUser:       comment.AdminUser,
				Account:         req.FromAccount,
				AccountPlatform: req.FromAccountPlatform,
				Currency:        fromCurrency,
				BeforeAmount:    fromBalance.String(),
				ChangeAmount:    amount.String(),
				ChangeMethod:    int32(comment.INNER_TRANS),
				TradeOrder:      &transferResp.OrderId,
			}
			var accountAdjustmentRes accountAdjust.CreateAccountAdjustmentRes
			err = tcc.CallBranch(accountAdjustmentReq,
				financeRpcFullAddr+financeRpc.AccountAdjustment_TccTryCreateAccountAdjustment_FullMethodName,
				financeRpcFullAddr+financeRpc.AccountAdjustment_TccConfirmCreateAccountAdjustment_FullMethodName,
				financeRpcFullAddr+financeRpc.AccountAdjustment_TccCancelCreateAccountAdjustment_FullMethodName,
				&accountAdjustmentRes)
			if err != nil {
				logx.WithContext(l.ctx).Errorf("账户调整记录失败,err:%v", err)
				return err
			}
			if accountAdjustmentRes.Status.Code != 0 {
				return common.NewErr(int(accountAdjustmentRes.Status.Code), accountAdjustmentRes.Status.Message)
			}
			payload := &finance_task.AccountAdjustBalance{
				ApplyId:      transferResp.OrderId,
				AdjustmentID: accountAdjustmentRes.OrderId,
			}
			taskInfo, _ := l.svcCtx.StaskClient.EnqueueContext(l.ctx, payload.CreateTransactionTaskFactory())
			logx.WithContext(l.ctx).Infof("task id: %+v", taskInfo.ID)
		}
		return nil
	})

4. 为什么要注册上下文?

为的是子事务屏障可以使用正确的上下文信息,子事务屏障如何使用呢?要从db入手

go 复制代码
package sql

import (
	"context"
	"database/sql"
	"fmt"

	"github.com/dtm-labs/client/dtmcli"
	"github.com/dtm-labs/client/dtmcli/dtmimp"
	"github.com/dtm-labs/client/dtmgrpc/dtmgimp"
	"github.com/zeromicro/go-zero/core/logx"
	"go.opentelemetry.io/otel/attribute"
	oteltrace "go.opentelemetry.io/otel/trace"
	"gorm.io/gorm"

	"violet/common"
)

type barrierDBWrapper struct {
	ctx context.Context
	DB  *gorm.DB
}

type sqlResult struct {
	rowsAffected int64
	err          error
}

func (ss *sqlResult) LastInsertId() (int64, error) {
	return 0, nil
}

// RowsAffected dtm_barrier 只会用了RowsAffected,没有使用 LastInsertId 所以没有实现 LastInsertId
func (ss *sqlResult) RowsAffected() (int64, error) {
	return ss.rowsAffected, ss.err
}

func NewDBWrapper(ctx context.Context, db *gorm.DB) *barrierDBWrapper {
	return &barrierDBWrapper{
		ctx: ctx,
		DB:  db,
	}
}

func (bb *barrierDBWrapper) Exec(query string, args ...interface{}) (sql.Result, error) {
	result := bb.DB.Exec(query, args...)
	return &sqlResult{
		rowsAffected: result.RowsAffected,
		err:          result.Error,
	}, result.Error
}

func (bb *barrierDBWrapper) QueryRow(query string, args ...interface{}) *sql.Row {
	return bb.DB.WithContext(bb.ctx).Raw(query, args...).Row()
}

type BarrierGormFunc func(gtx *gorm.DB) error

type DtmBarrier struct {
	ctx              context.Context
	TransType        string
	Gid              string
	BranchID         string
	Op               string
	BarrierID        int
	DBType           string // DBTypeMysql | DBTypePostgres
	BarrierTableName string
}

func NewBarrierFromGRPC(ctx context.Context) (*DtmBarrier, error) {
	tb := dtmgimp.TransBaseFromGrpc(ctx)
	barrier := &DtmBarrier{
		TransType: tb.TransType,
		Gid:       tb.Gid,
		BranchID:  tb.BranchID,
		Op:        tb.Op,
		ctx:       ctx,
	}

	if barrier.TransType == "" || barrier.Gid == "" || barrier.BranchID == "" || barrier.Op == "" {
		return nil, fmt.Errorf("invalid trans info: %v", barrier)
	}

	return barrier, nil
}

func (bb *DtmBarrier) String() string {
	return fmt.Sprintf("transInfo: %s %s %s %s", bb.TransType, bb.Gid, bb.BranchID, bb.Op)
}

func (bb *DtmBarrier) newBarrierID() string {
	bb.BarrierID++
	return fmt.Sprintf("%02d", bb.BarrierID)
}

// CallWithDB 结合gorm 对dtm barrier 的封装
// 注意:
//  1. BarrierGormFunc中对数据库的CREATE、UPDATE、DELETE必须使用参数gtx而不是d.DB, SELECT 两者都可以
//  2. CallWithDB 函数中使用gdb已经启动了一个事务,而且会自动提交,所以不必再BarrierGormFunc启动新事物
func (bb *DtmBarrier) CallWithDB(db *gorm.DB, busiCall BarrierGormFunc) (rerr error) {
	tx := db.WithContext(bb.ctx).Begin()
	defer dtmimp.DeferDo(&rerr, func() error {
		return tx.Commit().Error
	}, func() error {
		return tx.Rollback().Error
	})

	rerr = bb.Call(db, busiCall)
	return
}

func (bb *DtmBarrier) CallWithDBandTracing(db *gorm.DB, operationName string, busiCall BarrierGormFunc) (rerr error) {
	c, span := common.StartSpan(bb.ctx, "sql", func(r oteltrace.Span) {
		r.SetAttributes(attribute.Key("sql.method").String(operationName))
	})
	var err error
	defer func() {
		common.EndSpan(span, err)
	}()

	bb.ctx = c

	// 执行数据库操作并记录错误
	err = bb.CallWithDB(db, busiCall)

	return err
}

func (bb *DtmBarrier) Call(gtx *gorm.DB, busiCall BarrierGormFunc) (rerr error) {
	bid := bb.newBarrierID()
	originOp := map[string]string{
		dtmimp.OpCancel:     dtmimp.OpTry,    // tcc
		dtmimp.OpCompensate: dtmimp.OpAction, // saga
		dtmimp.OpRollback:   dtmimp.OpAction, // workflow
	}[bb.Op]

	originAffected, oerr := dtmimp.InsertBarrier(NewDBWrapper(bb.ctx, gtx), bb.TransType, bb.Gid, bb.BranchID, originOp, bid, bb.Op, bb.DBType, bb.BarrierTableName)
	currentAffected, rerr := dtmimp.InsertBarrier(NewDBWrapper(bb.ctx, gtx), bb.TransType, bb.Gid, bb.BranchID, bb.Op, bid, bb.Op, bb.DBType, bb.BarrierTableName)

	logx.WithContext(bb.ctx).Debugf("originAffected: %d currentAffected: %d", originAffected, currentAffected)

	if rerr == nil && bb.Op == dtmimp.MsgDoOp && currentAffected == 0 { // for msg's DoAndSubmit, repeated insert should be rejected.
		return dtmcli.ErrDuplicated
	}

	if rerr == nil {
		rerr = oerr
	}

	if (bb.Op == dtmimp.OpCancel || bb.Op == dtmimp.OpCompensate || bb.Op == dtmimp.OpRollback) && originAffected > 0 || // null compensate
		currentAffected == 0 { // repeated request or dangled request
		return
	}

	if rerr == nil {
		rerr = busiCall(gtx)
	}

	return
}

5. 使用子事务屏障

go 复制代码
func (d *DAO) ConfirmCreateDepositApply(ctx context.Context, apply *DepositApplication) (*DepositApplication, error) {
	barrier, err := sql.NewBarrierFromGRPC(ctx)
	if err != nil {
		logx.WithContext(ctx).Error(err)
		// codes.Internal 错误会重试
		return nil, err
	}
	//只修改状态
	err = barrier.CallWithDBandTracing(d.DB, "ConfirmCreateDepositApply", func(gtx *gorm.DB) error {
		err = gtx.Model(&DepositApplication{}).Where("gid = ? and trans_status = ?", barrier.Gid, common.DtmTransactionStatusPending).
			Update("trans_status", common.DtmTransactionStatusCompleted).Error
		if err != nil {
			return err
		}
		return nil
	})
	if err != nil {
		logx.Errorw("ConfirmCreateDepositApply error", logx.Field("err", err))
	}
	return apply, err
}

本教程到此为止,有什么不懂的请私信,有时间逐一解答

相关推荐
回家路上绕了弯4 小时前
用户中心微服务设计指南:从功能到非功能的全维度落地
后端·微服务
q***31834 小时前
微服务生态组件之Spring Cloud LoadBalancer详解和源码分析
java·spring cloud·微服务
Mgx6 小时前
一文讲透 Go 的 defer:你的“善后管家“,别让他变成“背锅侠“!
go
Mgx6 小时前
剪贴板监控记:用 Go 写一个 Windows 剪贴板监控器
go
陈果然DeepVersion7 小时前
Java大厂面试真题:从Spring Boot到AI微服务的三轮技术拷问
spring boot·redis·微服务·ai·智能客服·java面试·rag
百锦再13 小时前
第11章 泛型、trait与生命周期
android·网络·人工智能·python·golang·rust·go
小坏讲微服务16 小时前
Spring Boot整合Redis注解,实战Redis注解使用
spring boot·redis·分布式·后端·spring cloud·微服务·mybatis
周杰伦_Jay17 小时前
【智能体(Agent)技术深度解析】从架构到实现细节,核心是实现“感知环境→处理信息→决策行动→影响环境”的闭环
人工智能·机器学习·微服务·架构·golang·数据挖掘