158 lines
3.7 KiB
Go
158 lines
3.7 KiB
Go
package idempotency
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"chatapp3-golang/internal/model"
|
|
"chatapp3-golang/internal/utils"
|
|
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
const (
|
|
StatusProcessing = "PROCESSING"
|
|
StatusSucceeded = "SUCCEEDED"
|
|
StatusFailed = "FAILED"
|
|
|
|
MaxKeyLength = 128
|
|
)
|
|
|
|
var (
|
|
ErrEmptyKey = errors.New("idempotency: key is empty")
|
|
ErrKeyTooLong = errors.New("idempotency: key is too long")
|
|
ErrRequestMismatch = errors.New("idempotency: request hash mismatch")
|
|
)
|
|
|
|
type DB interface {
|
|
WithContext(ctx context.Context) *gorm.DB
|
|
}
|
|
|
|
type Store struct {
|
|
db DB
|
|
}
|
|
|
|
type BeginRequest struct {
|
|
Activity string
|
|
SysOrigin string
|
|
UserID int64
|
|
IdempotencyKey string
|
|
RequestHash string
|
|
}
|
|
|
|
type BeginResult struct {
|
|
Record model.ActivityIdempotency
|
|
Created bool
|
|
}
|
|
|
|
func NewStore(db DB) Store {
|
|
return Store{db: db}
|
|
}
|
|
|
|
func NormalizeKey(key string) (string, error) {
|
|
key = strings.TrimSpace(key)
|
|
if key == "" {
|
|
return "", ErrEmptyKey
|
|
}
|
|
if len(key) > MaxKeyLength {
|
|
return "", ErrKeyTooLong
|
|
}
|
|
return key, nil
|
|
}
|
|
|
|
func Hash(parts ...any) string {
|
|
payload, _ := json.Marshal(parts)
|
|
sum := sha256.Sum256(payload)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func (s Store) Begin(ctx context.Context, req BeginRequest) (BeginResult, error) {
|
|
key, err := NormalizeKey(req.IdempotencyKey)
|
|
if err != nil {
|
|
return BeginResult{}, err
|
|
}
|
|
now := time.Now()
|
|
id, err := utils.NextID()
|
|
if err != nil {
|
|
return BeginResult{}, err
|
|
}
|
|
row := model.ActivityIdempotency{
|
|
ID: id,
|
|
Activity: strings.ToUpper(strings.TrimSpace(req.Activity)),
|
|
SysOrigin: strings.ToUpper(strings.TrimSpace(req.SysOrigin)),
|
|
UserID: req.UserID,
|
|
IdempotencyKey: key,
|
|
RequestHash: req.RequestHash,
|
|
Status: StatusProcessing,
|
|
CreateTime: now,
|
|
UpdateTime: now,
|
|
}
|
|
result := s.db.WithContext(ctx).Clauses(clause.OnConflict{DoNothing: true}).Create(&row)
|
|
if result.Error != nil {
|
|
return BeginResult{}, result.Error
|
|
}
|
|
if result.RowsAffected == 1 {
|
|
return BeginResult{Record: row, Created: true}, nil
|
|
}
|
|
|
|
var existing model.ActivityIdempotency
|
|
err = s.db.WithContext(ctx).
|
|
Where("activity = ? AND sys_origin = ? AND user_id = ? AND idempotency_key = ?", row.Activity, row.SysOrigin, row.UserID, row.IdempotencyKey).
|
|
First(&existing).Error
|
|
if err != nil {
|
|
return BeginResult{}, err
|
|
}
|
|
if existing.RequestHash != req.RequestHash {
|
|
return BeginResult{}, ErrRequestMismatch
|
|
}
|
|
return BeginResult{Record: existing, Created: false}, nil
|
|
}
|
|
|
|
func (s Store) MarkSucceeded(ctx context.Context, id int64, businessNo string, response any) error {
|
|
responseJSON, err := json.Marshal(response)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return s.db.WithContext(ctx).Model(&model.ActivityIdempotency{}).
|
|
Where("id = ?", id).
|
|
Updates(map[string]any{
|
|
"business_no": strings.TrimSpace(businessNo),
|
|
"status": StatusSucceeded,
|
|
"response_json": string(responseJSON),
|
|
"error_message": "",
|
|
"update_time": time.Now(),
|
|
}).Error
|
|
}
|
|
|
|
func (s Store) MarkFailed(ctx context.Context, id int64, businessNo string, err error) error {
|
|
message := ""
|
|
if err != nil {
|
|
message = err.Error()
|
|
}
|
|
if len(message) > 1024 {
|
|
message = message[:1024]
|
|
}
|
|
return s.db.WithContext(ctx).Model(&model.ActivityIdempotency{}).
|
|
Where("id = ?", id).
|
|
Updates(map[string]any{
|
|
"business_no": strings.TrimSpace(businessNo),
|
|
"status": StatusFailed,
|
|
"error_message": message,
|
|
"update_time": time.Now(),
|
|
}).Error
|
|
}
|
|
|
|
func DecodeResponse(record model.ActivityIdempotency, out any) error {
|
|
if strings.TrimSpace(record.ResponseJSON) == "" {
|
|
return fmt.Errorf("idempotency response is empty")
|
|
}
|
|
return json.Unmarshal([]byte(record.ResponseJSON), out)
|
|
}
|