优化结构
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
# Integrations
|
||||
|
||||
`internal/integration` contains adapters that talk to systems outside the
|
||||
application process. They may open sockets, create SDK clients, keep reloadable
|
||||
state, or translate provider-specific protocols into `biz` interfaces.
|
||||
|
||||
## Packages
|
||||
|
||||
- `cache`: Redis-backed cache with an in-memory fallback.
|
||||
- `email`: SMTP email repository.
|
||||
- `payment`: payment-channel SDKs and callback/signature handling.
|
||||
- `storage`: local and object-storage implementations of `biz.FileStorage`.
|
||||
|
||||
These packages are not `utils`: they perform I/O and own external dependency
|
||||
lifecycles. Their constructors are exposed through `ProviderSet`; `cmd` binds
|
||||
the `cache.RedisProvider` implementation to the shared `data.Data` container.
|
||||
|
||||
Stateless protocol helpers that do not own clients live in
|
||||
`internal/utils/paymentutil`. They are intentionally small and dependency
|
||||
light, while provider adapters remain here.
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/pkg/paymentkit"
|
||||
"kra/internal/utils/paymentutil"
|
||||
|
||||
gopayAlipay "github.com/go-pay/gopay/alipay"
|
||||
gopayDouyin "github.com/go-pay/gopay/douyin"
|
||||
@@ -177,7 +177,7 @@ func TestWechatV2GoPayCallbackVerificationAndSandboxAmount(t *testing.T) {
|
||||
t.Errorf("read request: %v", err)
|
||||
return
|
||||
}
|
||||
requestValues, err = paymentkit.XMLValues(body)
|
||||
requestValues, err = paymentutil.XMLValues(body)
|
||||
if err != nil {
|
||||
t.Errorf("parse request: %v", err)
|
||||
return
|
||||
@@ -190,7 +190,7 @@ func TestWechatV2GoPayCallbackVerificationAndSandboxAmount(t *testing.T) {
|
||||
}
|
||||
response := map[string]string{"return_code": "SUCCESS", "return_msg": "OK", "result_code": "SUCCESS", "prepay_id": "prepay"}
|
||||
response["sign"] = wechatV2Sign(response, key, "MD5")
|
||||
_, _ = w.Write(paymentkit.XMLEncode(response))
|
||||
_, _ = w.Write(paymentutil.XMLEncode(response))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
@@ -219,7 +219,7 @@ func TestWechatV2GoPayCallbackVerificationAndSandboxAmount(t *testing.T) {
|
||||
if callbackValues["sign"] != originalSign {
|
||||
t.Fatal("verification mutated source values")
|
||||
}
|
||||
callbackBody := paymentkit.XMLEncode(callbackValues)
|
||||
callbackBody := paymentutil.XMLEncode(callbackValues)
|
||||
callbackResult, err := (&wechatV2Adapter{}).Callback(context.Background(), &biz.PaymentCallback{Body: callbackBody}, map[string]any{
|
||||
"app_id": "wx-test", "merchant_id": "mch-test", "mch_key": key,
|
||||
})
|
||||
@@ -243,7 +243,7 @@ func TestWechatV2MicropayUsesGoPaySDKAndValidatesIdentities(t *testing.T) {
|
||||
t.Errorf("read request: %v", err)
|
||||
return
|
||||
}
|
||||
requestValues, err = paymentkit.XMLValues(body)
|
||||
requestValues, err = paymentutil.XMLValues(body)
|
||||
if err != nil {
|
||||
t.Errorf("parse request: %v", err)
|
||||
return
|
||||
@@ -263,7 +263,7 @@ func TestWechatV2MicropayUsesGoPaySDKAndValidatesIdentities(t *testing.T) {
|
||||
"transaction_id": "WX-TRANSACTION-1", "total_fee": "123", "cash_fee": "123", "fee_type": "CNY",
|
||||
}
|
||||
response["sign"] = wechatV2Sign(response, key, "MD5")
|
||||
_, _ = w.Write(paymentkit.XMLEncode(response))
|
||||
_, _ = w.Write(paymentutil.XMLEncode(response))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
@@ -344,7 +344,7 @@ func TestWechatV2MicropayRejectsInvalidSuccessResponse(t *testing.T) {
|
||||
}
|
||||
tc.mutate(response)
|
||||
response["sign"] = wechatV2Sign(response, key, "MD5")
|
||||
_, _ = w.Write(paymentkit.XMLEncode(response))
|
||||
_, _ = w.Write(paymentutil.XMLEncode(response))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/pkg/paymentkit"
|
||||
"kra/internal/utils/paymentutil"
|
||||
|
||||
"github.com/go-pay/gopay"
|
||||
gopayQQ "github.com/go-pay/gopay/qq"
|
||||
@@ -55,7 +55,7 @@ func TestQQCreateKeepsClientPaymentDataOutOfDurableIdentities(t *testing.T) {
|
||||
http.Error(w, "invalid XML", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
values, err := paymentkit.XMLValues(body)
|
||||
values, err := paymentutil.XMLValues(body)
|
||||
if err != nil {
|
||||
t.Errorf("parse request XML: %v", err)
|
||||
http.Error(w, "invalid XML", http.StatusBadRequest)
|
||||
@@ -81,7 +81,7 @@ func TestQQCreateKeepsClientPaymentDataOutOfDurableIdentities(t *testing.T) {
|
||||
for key, value := range response {
|
||||
encoded[key] = fmt.Sprint(value)
|
||||
}
|
||||
_, _ = w.Write(paymentkit.XMLEncode(encoded))
|
||||
_, _ = w.Write(paymentutil.XMLEncode(encoded))
|
||||
}))
|
||||
|
||||
result, err := (&qqAdapter{}).Create(context.Background(), &biz.PaymentRequest{
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/pkg/paymentkit"
|
||||
"kra/internal/utils/paymentutil"
|
||||
)
|
||||
|
||||
func callbackFields(callback *biz.PaymentCallback) map[string]string {
|
||||
@@ -70,49 +70,49 @@ func firstAny(values map[string]any, keys ...string) string {
|
||||
}
|
||||
|
||||
// Compatibility shims keep channel adapters focused on protocol and
|
||||
// persistence concerns while stateless parsing rules live in paymentkit.
|
||||
// persistence concerns while stateless parsing rules live in paymentutil.
|
||||
func normalizePaymentStatus(value, fallback string) string {
|
||||
return paymentkit.NormalizeStatus(value, fallback)
|
||||
return paymentutil.NormalizeStatus(value, fallback)
|
||||
}
|
||||
|
||||
func parseIntegerAmount(value string) (int64, error) {
|
||||
return paymentkit.ParseIntegerAmount(value)
|
||||
return paymentutil.ParseIntegerAmount(value)
|
||||
}
|
||||
|
||||
func parseDecimalAmount(value string, scale int64) (int64, error) {
|
||||
return paymentkit.ParseDecimalAmount(value, scale)
|
||||
return paymentutil.ParseDecimalAmount(value, scale)
|
||||
}
|
||||
|
||||
func formatDecimalAmount(amount, scale int64) (string, error) {
|
||||
return paymentkit.FormatDecimalAmount(amount, scale)
|
||||
return paymentutil.FormatDecimalAmount(amount, scale)
|
||||
}
|
||||
|
||||
func configuredInt64(values map[string]any, key string, fallback int64) int64 {
|
||||
return paymentkit.ConfiguredInt64(values, key, fallback)
|
||||
return paymentutil.ConfiguredInt64(values, key, fallback)
|
||||
}
|
||||
|
||||
func jsonObject(raw []byte) map[string]any {
|
||||
return paymentkit.JSONObject(raw)
|
||||
return paymentutil.JSONObject(raw)
|
||||
}
|
||||
|
||||
func nestedString(value any, keys ...string) string {
|
||||
return paymentkit.NestedString(value, keys...)
|
||||
return paymentutil.NestedString(value, keys...)
|
||||
}
|
||||
|
||||
func valueAtPath(value any, path string) any {
|
||||
return paymentkit.ValueAtPath(value, path)
|
||||
return paymentutil.ValueAtPath(value, path)
|
||||
}
|
||||
|
||||
func stringAtPath(value any, path string) string {
|
||||
return paymentkit.StringAtPath(value, path)
|
||||
return paymentutil.StringAtPath(value, path)
|
||||
}
|
||||
|
||||
func configuredValues(values map[string]any, key string) []string {
|
||||
return paymentkit.ConfiguredValues(values, key)
|
||||
return paymentutil.ConfiguredValues(values, key)
|
||||
}
|
||||
|
||||
func containsFold(values []string, value string) bool {
|
||||
return paymentkit.ContainsFold(values, value)
|
||||
return paymentutil.ContainsFold(values, value)
|
||||
}
|
||||
|
||||
// parseConfiguredAmount reads a provider amount field using an explicit unit
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
"time"
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/pkg/paymentkit"
|
||||
"kra/internal/utils/paymentutil"
|
||||
)
|
||||
|
||||
type vendorProfile string
|
||||
@@ -244,18 +244,18 @@ func (a *vendorPaymentAdapter) signRequest(request *http.Request, payload map[st
|
||||
digest := sha256.Sum256([]byte(appID + timestamp + nonceValue + string(raw) + secret))
|
||||
request.Header.Set("Authorization", "OPEN-BODY-SIG AppId="+appID+", Timestamp="+timestamp+", Nonce="+nonceValue+", Signature="+hex.EncodeToString(digest[:]))
|
||||
case vendorSFT:
|
||||
request.Header.Set("X-SFT-Sign", paymentkit.MD5Canonical(payload, secret))
|
||||
request.Header.Set("X-SFT-Sign", paymentutil.MD5Canonical(payload, secret))
|
||||
case vendorWechatGame:
|
||||
request.Header.Set("X-Wechat-Game-Sign", paymentkit.HMACSHA256Hex(raw, secret, false))
|
||||
request.Header.Set("X-Wechat-Game-Sign", paymentutil.HMACSHA256Hex(raw, secret, false))
|
||||
if token := text(c, "access_token"); token != "" {
|
||||
query := request.URL.Query()
|
||||
query.Set("access_token", token)
|
||||
request.URL.RawQuery = query.Encode()
|
||||
}
|
||||
case vendorDouyinGame:
|
||||
request.Header.Set("X-TT-Pay-Sign", paymentkit.MD5Canonical(payload, secret))
|
||||
request.Header.Set("X-TT-Pay-Sign", paymentutil.MD5Canonical(payload, secret))
|
||||
default:
|
||||
request.Header.Set("X-Payment-Sign", paymentkit.HMACSHA256Hex(raw, secret, false))
|
||||
request.Header.Set("X-Payment-Sign", paymentutil.HMACSHA256Hex(raw, secret, false))
|
||||
}
|
||||
}
|
||||
func (a *vendorPaymentAdapter) verify(fields map[string]string, raw []byte, headers map[string]string, c map[string]any) error {
|
||||
@@ -298,9 +298,9 @@ func (a *vendorPaymentAdapter) verify(fields map[string]string, raw []byte, head
|
||||
values[key] = value
|
||||
}
|
||||
}
|
||||
actual = paymentkit.MD5Canonical(values, secret)
|
||||
actual = paymentutil.MD5Canonical(values, secret)
|
||||
default:
|
||||
actual = paymentkit.HMACSHA256Hex(raw, secret, false)
|
||||
actual = paymentutil.HMACSHA256Hex(raw, secret, false)
|
||||
}
|
||||
if !hmac.Equal([]byte(strings.ToLower(expected)), []byte(strings.ToLower(actual))) {
|
||||
return errors.New("支付回调签名校验失败")
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// Package integration contains adapters for external systems and runtime
|
||||
// clients. These constructors are kept out of the persistence provider set so
|
||||
// the data package does not also act as the integration composition root.
|
||||
package integration
|
||||
|
||||
import (
|
||||
"kra/internal/biz"
|
||||
"kra/internal/integration/cache"
|
||||
"kra/internal/integration/email"
|
||||
"kra/internal/integration/storage"
|
||||
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
var ProviderSet = wire.NewSet(
|
||||
cache.New,
|
||||
email.NewEmailRepo,
|
||||
storage.NewFileStorage,
|
||||
wire.Bind(new(biz.FileStorage), new(*storage.Reloadable)),
|
||||
)
|
||||
@@ -0,0 +1,56 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
)
|
||||
|
||||
// composeStreams concatenates objects into destination while calculating the
|
||||
// MD5 of the exact bytes written. Provider-specific storage clients stay out
|
||||
// of this package; callbacks keep this helper independent from biz and SDKs.
|
||||
func composeStreams(
|
||||
ctx context.Context,
|
||||
names []string,
|
||||
open func(context.Context, string) (io.ReadCloser, error),
|
||||
put func(context.Context, string, io.Reader) error,
|
||||
remove func(context.Context, string) error,
|
||||
destination string,
|
||||
) (string, error) {
|
||||
reader, writer := io.Pipe()
|
||||
hash := md5.New()
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
defer writer.Close()
|
||||
for _, name := range names {
|
||||
if err := ctx.Err(); err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
file, err := open(ctx, name)
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
return
|
||||
}
|
||||
_, copyErr := io.Copy(io.MultiWriter(writer, hash), file)
|
||||
_ = file.Close()
|
||||
if copyErr != nil {
|
||||
errCh <- copyErr
|
||||
return
|
||||
}
|
||||
}
|
||||
errCh <- nil
|
||||
}()
|
||||
putErr := put(ctx, destination, reader)
|
||||
composeErr := <-errCh
|
||||
if putErr != nil {
|
||||
_ = remove(ctx, destination)
|
||||
return "", putErr
|
||||
}
|
||||
if composeErr != nil {
|
||||
_ = remove(ctx, destination)
|
||||
return "", composeErr
|
||||
}
|
||||
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestComposeStreams(t *testing.T) {
|
||||
objects := map[string]string{"a": "hello ", "b": "world"}
|
||||
var stored string
|
||||
hash, err := composeStreams(context.Background(), []string{"a", "b"}, func(_ context.Context, name string) (io.ReadCloser, error) {
|
||||
value, ok := objects[name]
|
||||
if !ok {
|
||||
return nil, errors.New("missing object")
|
||||
}
|
||||
return io.NopCloser(strings.NewReader(value)), nil
|
||||
}, func(_ context.Context, _ string, reader io.Reader) error {
|
||||
value, err := io.ReadAll(reader)
|
||||
stored = string(value)
|
||||
return err
|
||||
}, func(context.Context, string) error { return nil }, "out")
|
||||
if err != nil || stored != "hello world" {
|
||||
t.Fatalf("compose = %q, %v", stored, err)
|
||||
}
|
||||
want := md5.Sum([]byte(stored))
|
||||
if hash != hex.EncodeToString(want[:]) {
|
||||
t.Fatalf("hash = %q", hash)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComposeStreamsRemovesPartialDestination(t *testing.T) {
|
||||
removed := false
|
||||
_, err := composeStreams(context.Background(), []string{"missing"}, func(context.Context, string) (io.ReadCloser, error) {
|
||||
return nil, errors.New("open failed")
|
||||
}, func(_ context.Context, _ string, reader io.Reader) error {
|
||||
_, _ = io.ReadAll(reader)
|
||||
return nil
|
||||
}, func(context.Context, string) error {
|
||||
removed = true
|
||||
return nil
|
||||
}, "out")
|
||||
if err == nil || !removed {
|
||||
t.Fatalf("compose error = %v, removed = %v", err, removed)
|
||||
}
|
||||
}
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
|
||||
"kra/internal/biz"
|
||||
"kra/internal/conf"
|
||||
"kra/pkg/osskit"
|
||||
)
|
||||
|
||||
type fileStorage struct {
|
||||
@@ -66,7 +65,7 @@ func New(config *conf.AdminBackend) (biz.FileStorage, error) {
|
||||
|
||||
func composeFiles(ctx context.Context, storage biz.FileStorage, names []string, destination string) (*biz.StoredFile, string, error) {
|
||||
var stored *biz.StoredFile
|
||||
hash, err := osskit.ComposeStreams(ctx, names, storage.Open, func(ctx context.Context, destination string, reader io.Reader) error {
|
||||
hash, err := composeStreams(ctx, names, storage.Open, func(ctx context.Context, destination string, reader io.Reader) error {
|
||||
var putErr error
|
||||
stored, putErr = storage.Put(ctx, destination, reader)
|
||||
return putErr
|
||||
|
||||
Reference in New Issue
Block a user