优化结构
This commit is contained in:
parent
361a562f75
commit
13f4589a18
|
|
@ -1,6 +1,6 @@
|
||||||
package payment
|
package payment
|
||||||
|
|
||||||
import "kra/app/system/internal/utils/paymentutil"
|
import "kra/pkg/paymentkit"
|
||||||
|
|
||||||
func text(values map[string]any, key string) string {
|
func text(values map[string]any, key string) string {
|
||||||
value, _ := values[key].(string)
|
value, _ := values[key].(string)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# Integrations
|
# Integrations
|
||||||
|
|
||||||
`internal/integration` contains adapters that talk to systems outside the
|
`app/system/internal/integration` contains adapters that talk to systems outside the
|
||||||
application process. They may open sockets, create SDK clients, keep reloadable
|
application process. They may open sockets, create SDK clients, keep reloadable
|
||||||
state, or translate provider-specific protocols into `biz` interfaces.
|
state, or translate provider-specific protocols into `biz` interfaces.
|
||||||
|
|
||||||
|
|
@ -16,5 +16,5 @@ lifecycles. Their constructors are exposed through `ProviderSet`; `cmd` binds
|
||||||
the `cache.RedisProvider` implementation to the shared `data.Data` container.
|
the `cache.RedisProvider` implementation to the shared `data.Data` container.
|
||||||
|
|
||||||
Stateless protocol helpers that do not own clients live in
|
Stateless protocol helpers that do not own clients live in
|
||||||
`internal/utils/paymentutil`. They are intentionally small and dependency
|
`pkg/paymentkit`. They are intentionally small and dependency
|
||||||
light, while provider adapters remain here.
|
light, while provider adapters remain here.
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"kra/app/system/internal/biz"
|
"kra/app/system/internal/biz"
|
||||||
"kra/app/system/internal/utils/paymentutil"
|
"kra/pkg/paymentkit"
|
||||||
|
|
||||||
gopayAlipay "github.com/go-pay/gopay/alipay"
|
gopayAlipay "github.com/go-pay/gopay/alipay"
|
||||||
gopayDouyin "github.com/go-pay/gopay/douyin"
|
gopayDouyin "github.com/go-pay/gopay/douyin"
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"kra/app/system/internal/biz"
|
"kra/app/system/internal/biz"
|
||||||
"kra/app/system/internal/utils/paymentutil"
|
"kra/pkg/paymentkit"
|
||||||
|
|
||||||
"github.com/go-pay/gopay"
|
"github.com/go-pay/gopay"
|
||||||
gopayQQ "github.com/go-pay/gopay/qq"
|
gopayQQ "github.com/go-pay/gopay/qq"
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"kra/app/system/internal/biz"
|
"kra/app/system/internal/biz"
|
||||||
"kra/app/system/internal/utils/paymentutil"
|
"kra/pkg/paymentkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
func callbackFields(callback *biz.PaymentCallback) map[string]string {
|
func callbackFields(callback *biz.PaymentCallback) map[string]string {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"kra/app/system/internal/biz"
|
"kra/app/system/internal/biz"
|
||||||
"kra/app/system/internal/utils/paymentutil"
|
"kra/pkg/paymentkit"
|
||||||
)
|
)
|
||||||
|
|
||||||
type vendorProfile string
|
type vendorProfile string
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,29 @@
|
||||||
package httpx
|
package httpx
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
shared "kra/pkg/httpx"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
CodeSuccess = 0
|
CodeSuccess = shared.CodeSuccess
|
||||||
CodeError = 7
|
CodeError = shared.CodeError
|
||||||
CodePasswordChangeRequired = 10001
|
CodePasswordChangeRequired = shared.CodePasswordChangeRequired
|
||||||
)
|
)
|
||||||
|
|
||||||
type Response struct {
|
type Response = shared.Response
|
||||||
Code int `json:"code"`
|
type PageResult = shared.PageResult
|
||||||
Data any `json:"data"`
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PageResult struct {
|
|
||||||
List any `json:"list"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
Page int `json:"page"`
|
|
||||||
PageSize int `json:"pageSize"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func Write(c *gin.Context, code int, data any, message string) {
|
func Write(c *gin.Context, code int, data any, message string) {
|
||||||
c.JSON(http.StatusOK, Response{Code: code, Data: data, Msg: message})
|
shared.Write(c, code, data, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
func OK(c *gin.Context) { Write(c, CodeSuccess, gin.H{}, "操作成功") }
|
func OK(c *gin.Context) { shared.OK(c) }
|
||||||
|
|
||||||
func OKWithData(c *gin.Context, data any) { Write(c, CodeSuccess, data, "成功") }
|
func OKWithData(c *gin.Context, data any) { shared.OKWithData(c, data) }
|
||||||
|
|
||||||
func Fail(c *gin.Context, message string) { Write(c, CodeError, gin.H{}, message) }
|
func Fail(c *gin.Context, message string) { shared.Fail(c, message) }
|
||||||
|
|
||||||
func NoAuth(c *gin.Context, message string) {
|
func NoAuth(c *gin.Context, message string) {
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, Response{Code: CodeError, Data: nil, Msg: message})
|
shared.NoAuth(c, message)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,8 @@ package configutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"strings"
|
|
||||||
|
"kra/pkg/protoutil"
|
||||||
|
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
|
|
@ -11,68 +12,5 @@ import (
|
||||||
// MergeProtoJSON applies a JSON object as a partial protobuf update while
|
// MergeProtoJSON applies a JSON object as a partial protobuf update while
|
||||||
// retaining fields omitted by the caller.
|
// retaining fields omitted by the caller.
|
||||||
func MergeProtoJSON(target proto.Message, patch json.RawMessage, options protojson.UnmarshalOptions) error {
|
func MergeProtoJSON(target proto.Message, patch json.RawMessage, options protojson.UnmarshalOptions) error {
|
||||||
currentRaw, err := protojson.MarshalOptions{UseProtoNames: false}.Marshal(target)
|
return protoutil.MergeProtoJSON(target, patch, options)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var current map[string]any
|
|
||||||
if err = json.Unmarshal(currentRaw, ¤t); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var incoming map[string]any
|
|
||||||
if err = json.Unmarshal(patch, &incoming); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
incoming = normalizeJSONKeys(incoming).(map[string]any)
|
|
||||||
mergeJSONObjects(current, incoming)
|
|
||||||
merged, err := json.Marshal(current)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return options.Unmarshal(merged, target)
|
|
||||||
}
|
|
||||||
|
|
||||||
func normalizeJSONKeys(value any) any {
|
|
||||||
switch item := value.(type) {
|
|
||||||
case map[string]any:
|
|
||||||
result := make(map[string]any, len(item))
|
|
||||||
for key, nested := range item {
|
|
||||||
result[snakeToLowerCamel(key)] = normalizeJSONKeys(nested)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
case []any:
|
|
||||||
result := make([]any, len(item))
|
|
||||||
for index, nested := range item {
|
|
||||||
result[index] = normalizeJSONKeys(nested)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
default:
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func snakeToLowerCamel(value string) string {
|
|
||||||
if !strings.Contains(value, "_") {
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
parts := strings.Split(value, "_")
|
|
||||||
result := parts[0]
|
|
||||||
for _, part := range parts[1:] {
|
|
||||||
if part != "" {
|
|
||||||
result += strings.ToUpper(part[:1]) + part[1:]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func mergeJSONObjects(target, patch map[string]any) {
|
|
||||||
for key, value := range patch {
|
|
||||||
if incoming, ok := value.(map[string]any); ok {
|
|
||||||
if existing, exists := target[key].(map[string]any); exists {
|
|
||||||
mergeJSONObjects(existing, incoming)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
target[key] = value
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
# Payment Utilities
|
|
||||||
|
|
||||||
Stateless payment protocol helpers live here: amount conversion, status
|
|
||||||
normalization, JSON/XML extraction, and request signing. Provider adapters and
|
|
||||||
their SDK clients remain under `internal/integration/payment`.
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
package paymentutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ParseIntegerAmount parses an integer amount expressed in the smallest
|
|
||||||
// currency unit. Floating point is intentionally not involved.
|
|
||||||
func ParseIntegerAmount(value string) (int64, error) {
|
|
||||||
value = strings.TrimSpace(value)
|
|
||||||
if value == "" {
|
|
||||||
return 0, errors.New("支付金额为空")
|
|
||||||
}
|
|
||||||
return strconv.ParseInt(value, 10, 64)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseDecimalAmount converts a major-unit decimal string to an integer amount
|
|
||||||
// using a power-of-ten scale, without using floating point.
|
|
||||||
func ParseDecimalAmount(value string, scale int64) (int64, error) {
|
|
||||||
value = strings.TrimSpace(value)
|
|
||||||
if value == "" || scale <= 0 {
|
|
||||||
return 0, errors.New("支付金额或换算比例无效")
|
|
||||||
}
|
|
||||||
if strings.HasPrefix(value, "-") {
|
|
||||||
return 0, errors.New("支付金额不能为负数")
|
|
||||||
}
|
|
||||||
parts := strings.Split(value, ".")
|
|
||||||
if len(parts) > 2 || parts[0] == "" {
|
|
||||||
return 0, fmt.Errorf("支付金额格式错误: %s", value)
|
|
||||||
}
|
|
||||||
digits, err := scaleDigits(scale)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
whole, err := strconv.ParseInt(parts[0], 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
fraction := ""
|
|
||||||
if len(parts) == 2 {
|
|
||||||
fraction = parts[1]
|
|
||||||
}
|
|
||||||
if len(fraction) > digits {
|
|
||||||
return 0, fmt.Errorf("支付金额精度超过 %d 位", digits)
|
|
||||||
}
|
|
||||||
fraction += strings.Repeat("0", digits-len(fraction))
|
|
||||||
fractionValue := int64(0)
|
|
||||||
if fraction != "" {
|
|
||||||
fractionValue, err = strconv.ParseInt(fraction, 10, 64)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if whole > (int64(^uint64(0)>>1)-fractionValue)/scale {
|
|
||||||
return 0, errors.New("支付金额超出范围")
|
|
||||||
}
|
|
||||||
return whole*scale + fractionValue, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func FormatDecimalAmount(amount, scale int64) (string, error) {
|
|
||||||
if amount <= 0 || scale <= 0 {
|
|
||||||
return "", errors.New("支付金额或换算比例无效")
|
|
||||||
}
|
|
||||||
digits, err := scaleDigits(scale)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if digits == 0 {
|
|
||||||
return strconv.FormatInt(amount, 10), nil
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%d.%0*d", amount/scale, digits, amount%scale), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func scaleDigits(scale int64) (int, error) {
|
|
||||||
digits := 0
|
|
||||||
for n := scale; n > 1 && n%10 == 0; n /= 10 {
|
|
||||||
digits++
|
|
||||||
}
|
|
||||||
if scale <= 0 || scale != pow10(digits) {
|
|
||||||
return 0, errors.New("支付金额换算比例必须是 10 的幂")
|
|
||||||
}
|
|
||||||
return digits, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func pow10(digits int) int64 {
|
|
||||||
value := int64(1)
|
|
||||||
for i := 0; i < digits; i++ {
|
|
||||||
if value > (int64(^uint64(0)>>1))/10 {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
value *= 10
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
package paymentutil
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestDecimalAmountUsesIntegerMath(t *testing.T) {
|
|
||||||
got, err := ParseDecimalAmount("12.34", 100)
|
|
||||||
if err != nil || got != 1234 {
|
|
||||||
t.Fatalf("ParseDecimalAmount = %d, %v", got, err)
|
|
||||||
}
|
|
||||||
formatted, err := FormatDecimalAmount(got, 100)
|
|
||||||
if err != nil || formatted != "12.34" {
|
|
||||||
t.Fatalf("FormatDecimalAmount = %q, %v", formatted, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDecimalAmountRejectsUnsafeInput(t *testing.T) {
|
|
||||||
for _, value := range []string{"-1.00", "1.001", "1.2.3"} {
|
|
||||||
if _, err := ParseDecimalAmount(value, 100); err == nil {
|
|
||||||
t.Fatalf("ParseDecimalAmount(%q) accepted unsafe input", value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if _, err := ParseDecimalAmount("1", 3); err == nil {
|
|
||||||
t.Fatal("non power-of-ten scale accepted")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
package paymentutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func JSONObject(raw []byte) map[string]any {
|
|
||||||
var value any
|
|
||||||
if json.Unmarshal(raw, &value) != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
object, _ := value.(map[string]any)
|
|
||||||
return object
|
|
||||||
}
|
|
||||||
|
|
||||||
func NestedString(value any, keys ...string) string {
|
|
||||||
found := nestedValue(value, keys...)
|
|
||||||
if found == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(fmt.Sprint(found))
|
|
||||||
}
|
|
||||||
|
|
||||||
func StringAtPath(value any, path string) string {
|
|
||||||
found := ValueAtPath(value, path)
|
|
||||||
if found == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(fmt.Sprint(found))
|
|
||||||
}
|
|
||||||
|
|
||||||
func ValueAtPath(value any, path string) any {
|
|
||||||
path = strings.TrimSpace(path)
|
|
||||||
if path == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
current := value
|
|
||||||
for _, part := range strings.Split(path, ".") {
|
|
||||||
object, ok := current.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
current = object[part]
|
|
||||||
}
|
|
||||||
return current
|
|
||||||
}
|
|
||||||
|
|
||||||
func nestedValue(value any, keys ...string) any {
|
|
||||||
object, ok := value.(map[string]any)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, key := range keys {
|
|
||||||
if found, exists := object[key]; exists && found != nil && fmt.Sprint(found) != "" {
|
|
||||||
return found
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, nestedKey := range []string{"data", "result", "order", "transaction", "response"} {
|
|
||||||
if found := nestedValue(object[nestedKey], keys...); found != nil {
|
|
||||||
return found
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
package paymentutil
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestNestedAndPathValues(t *testing.T) {
|
|
||||||
object := JSONObject([]byte(`{"data":{"status":"SUCCESS","amount":100}}`))
|
|
||||||
if got := NestedString(object, "status"); got != "SUCCESS" {
|
|
||||||
t.Fatalf("NestedString = %q", got)
|
|
||||||
}
|
|
||||||
if got := StringAtPath(object, "data.amount"); got != "100" {
|
|
||||||
t.Fatalf("StringAtPath = %q", got)
|
|
||||||
}
|
|
||||||
if got := StringAtPath(object, "data.missing"); got != "" {
|
|
||||||
t.Fatalf("missing path = %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
package paymentutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/hmac"
|
|
||||||
"crypto/md5"
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"fmt"
|
|
||||||
"net/url"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func WechatV2Sign(values map[string]string, key, signType string) string {
|
|
||||||
keys := make([]string, 0, len(values))
|
|
||||||
for k, v := range values {
|
|
||||||
if k != "sign" && v != "" {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
parts := make([]string, 0, len(keys))
|
|
||||||
for _, k := range keys {
|
|
||||||
parts = append(parts, k+"="+values[k])
|
|
||||||
}
|
|
||||||
raw := strings.Join(parts, "&") + "&key=" + key
|
|
||||||
if strings.EqualFold(signType, "HMAC-SHA256") {
|
|
||||||
return HMACSHA256Hex([]byte(raw), key, true)
|
|
||||||
}
|
|
||||||
sum := md5.Sum([]byte(raw))
|
|
||||||
return strings.ToUpper(hex.EncodeToString(sum[:]))
|
|
||||||
}
|
|
||||||
|
|
||||||
func MD5Canonical(values map[string]any, secret string) string {
|
|
||||||
keys := make([]string, 0, len(values))
|
|
||||||
for key, value := range values {
|
|
||||||
if key != "sign" && value != nil && fmt.Sprint(value) != "" {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
query := url.Values{}
|
|
||||||
for _, key := range keys {
|
|
||||||
query.Set(key, fmt.Sprint(values[key]))
|
|
||||||
}
|
|
||||||
sum := md5.Sum([]byte(query.Encode() + "&key=" + secret))
|
|
||||||
return strings.ToUpper(hex.EncodeToString(sum[:]))
|
|
||||||
}
|
|
||||||
|
|
||||||
func HMACSHA256Hex(raw []byte, secret string, upper bool) string {
|
|
||||||
h := hmac.New(sha256.New, []byte(secret))
|
|
||||||
_, _ = h.Write(raw)
|
|
||||||
result := hex.EncodeToString(h.Sum(nil))
|
|
||||||
if upper {
|
|
||||||
return strings.ToUpper(result)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
package paymentutil
|
|
||||||
|
|
||||||
import "testing"
|
|
||||||
|
|
||||||
func TestWechatV2SignIsStableAndExcludesSign(t *testing.T) {
|
|
||||||
values := map[string]string{"b": "2", "a": "1", "sign": "old"}
|
|
||||||
first := WechatV2Sign(values, "secret", "MD5")
|
|
||||||
values["sign"] = "different"
|
|
||||||
if second := WechatV2Sign(values, "secret", "MD5"); first != second {
|
|
||||||
t.Fatalf("signature changed when sign field changed: %q != %q", first, second)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
package paymentutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func NormalizeStatus(value, fallback string) string {
|
|
||||||
switch strings.ToUpper(strings.TrimSpace(value)) {
|
|
||||||
case "SUCCESS", "PAID", "TRADE_SUCCESS", "TRADE_FINISHED", "COMPLETED", "PAY_SUCCESS":
|
|
||||||
return "success"
|
|
||||||
case "WAIT_BUYER_PAY", "USERPAYING", "NOTPAY", "PROCESSING", "PENDING", "CREATED", "ACCEPT", "PAYING":
|
|
||||||
return "pending"
|
|
||||||
case "CLOSED", "TRADE_CLOSED", "CANCELLED", "CANCELED", "REVOKED", "REFUND", "REFUNDED", "FAILED", "FAIL", "PAYERROR":
|
|
||||||
return "failed"
|
|
||||||
default:
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ConfiguredInt64(values map[string]any, key string, fallback int64) int64 {
|
|
||||||
value, ok := values[key]
|
|
||||||
if !ok {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
parsed, err := ParseIntegerAmount(strings.TrimSpace(toString(value)))
|
|
||||||
if err != nil || parsed <= 0 {
|
|
||||||
return fallback
|
|
||||||
}
|
|
||||||
return parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
func ConfiguredValues(values map[string]any, key string) []string {
|
|
||||||
result := []string{}
|
|
||||||
for _, item := range strings.Split(toString(values[key]), ",") {
|
|
||||||
item = strings.ToUpper(strings.TrimSpace(item))
|
|
||||||
if item != "" {
|
|
||||||
result = append(result, item)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func ContainsFold(values []string, value string) bool {
|
|
||||||
value = strings.ToUpper(strings.TrimSpace(value))
|
|
||||||
for _, item := range values {
|
|
||||||
if item == value {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func toString(value any) string {
|
|
||||||
if value == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(strings.ReplaceAll(strings.TrimSpace(stringify(value)), "<nil>", ""))
|
|
||||||
}
|
|
||||||
|
|
||||||
func stringify(value any) string {
|
|
||||||
return fmt.Sprint(value)
|
|
||||||
}
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
package paymentutil
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/xml"
|
|
||||||
"io"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type xmlValue struct {
|
|
||||||
XMLName xml.Name
|
|
||||||
Value string `xml:",chardata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func XMLValues(raw []byte) (map[string]string, error) {
|
|
||||||
decoder := xml.NewDecoder(bytes.NewReader(raw))
|
|
||||||
result := map[string]string{}
|
|
||||||
for {
|
|
||||||
token, err := decoder.Token()
|
|
||||||
if err == io.EOF {
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
start, ok := token.(xml.StartElement)
|
|
||||||
if !ok || start.Name.Local == "xml" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
var value xmlValue
|
|
||||||
if err = decoder.DecodeElement(&value, &start); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if value.Value != "" {
|
|
||||||
result[start.Name.Local] = value.Value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func XMLEncode(values map[string]string) []byte {
|
|
||||||
var b strings.Builder
|
|
||||||
b.WriteString("<xml>")
|
|
||||||
keys := make([]string, 0, len(values))
|
|
||||||
for key := range values {
|
|
||||||
keys = append(keys, key)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
for _, key := range keys {
|
|
||||||
b.WriteString("<")
|
|
||||||
b.WriteString(key)
|
|
||||||
b.WriteString("><![CDATA[")
|
|
||||||
b.WriteString(values[key])
|
|
||||||
b.WriteString("]]></")
|
|
||||||
b.WriteString(key)
|
|
||||||
b.WriteString(">")
|
|
||||||
}
|
|
||||||
b.WriteString("</xml>")
|
|
||||||
return []byte(b.String())
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
# Platform
|
# Shared Packages
|
||||||
|
|
||||||
这里放跨业务模块的基础设施,不放具体业务表或业务用例。
|
这里放跨业务模块的基础设施,不放具体业务表或业务用例。
|
||||||
|
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
- `database`:跨模块共享的 GORM 支持、分页和迁移执行器
|
- `database`:跨模块共享的 GORM 支持、分页和迁移执行器
|
||||||
- `module`:模块迁移、后台元数据和路由注册协议
|
- `module`:模块迁移、后台元数据和路由注册协议
|
||||||
- `task`:跨模块共享的进程内任务注册表和贡献协议
|
- `task`:跨模块共享的进程内任务注册表和贡献协议
|
||||||
|
- `logging`:跨 app 复用的结构化日志能力
|
||||||
|
- `paymentkit`:支付金额、签名、状态和报文处理纯函数
|
||||||
|
|
||||||
平台层只能提供机制,不能引用 `internal/modules/*`。数据库配置加载、系统集成配置
|
`pkg` 只能提供机制和稳定协议,不能引用任何 `app/*/internal`。数据库配置加载、
|
||||||
和系统表仍由 system 模块负责;未来提炼公共连接管理时,也必须先形成不带系统
|
系统集成配置、系统表和 provider 生命周期仍由 `app/system` 负责。
|
||||||
业务语义的稳定接口,再移动到这里。
|
|
||||||
|
|
|
||||||
|
|
@ -50,9 +50,9 @@ func skipStackFile(filename string) bool {
|
||||||
"/go/pkg/mod/",
|
"/go/pkg/mod/",
|
||||||
"/go.uber.org/",
|
"/go.uber.org/",
|
||||||
"/gorm.io/",
|
"/gorm.io/",
|
||||||
"/internal/logging/",
|
"/pkg/logging/",
|
||||||
"/internal/server/middleware/",
|
"/internal/transport/middleware/",
|
||||||
"/internal/server/router/",
|
"/internal/transport/router/",
|
||||||
} {
|
} {
|
||||||
if strings.Contains(normalized, marker) {
|
if strings.Contains(normalized, marker) {
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ type Options struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ErrorEntry is the storage-neutral representation of an Error-level log.
|
// ErrorEntry is the storage-neutral representation of an Error-level log.
|
||||||
// Keeping it in internal/logging lets the log core report failures without taking a
|
// Keeping it in pkg/logging lets the log core report failures without taking a
|
||||||
// dependency on the application service or persistence layers.
|
// dependency on the application service or persistence layers.
|
||||||
type ErrorEntry struct {
|
type ErrorEntry struct {
|
||||||
Form, Info, Level, RequestID, TraceID string
|
Form, Info, Level, RequestID, TraceID string
|
||||||
|
|
@ -264,7 +264,7 @@ func isGORMLoggerEntry(filename string, fields []zapcore.Field) bool {
|
||||||
}
|
}
|
||||||
normalized := strings.ReplaceAll(filename, "\\", "/")
|
normalized := strings.ReplaceAll(filename, "\\", "/")
|
||||||
return strings.HasSuffix(normalized, "/gorm_logger_writer.go") ||
|
return strings.HasSuffix(normalized, "/gorm_logger_writer.go") ||
|
||||||
strings.HasSuffix(normalized, "/internal/data/gorm_logger.go")
|
strings.HasSuffix(normalized, "/pkg/database/gormkit/logger.go")
|
||||||
}
|
}
|
||||||
|
|
||||||
func errorEntryFromZap(entry zapcore.Entry, fields []zapcore.Field) ErrorEntry {
|
func errorEntryFromZap(entry zapcore.Entry, fields []zapcore.Field) ErrorEntry {
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ func TestErrorSinkSkipsGORMBridge(t *testing.T) {
|
||||||
base := zapcore.NewNopCore()
|
base := zapcore.NewNopCore()
|
||||||
core := &routedFileCore{base: base, encoder: zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), level: zapcore.ErrorLevel, root: root, state: &routedFileState{writers: map[string]*DailyWriter{}}, errorSink: state}
|
core := &routedFileCore{base: base, encoder: zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()), level: zapcore.ErrorLevel, root: root, state: &routedFileState{writers: map[string]*DailyWriter{}}, errorSink: state}
|
||||||
defer core.Close()
|
defer core.Close()
|
||||||
for _, filename := range []string{"/tmp/gorm_logger_writer.go", "/workspace/internal/data/gorm_logger.go"} {
|
for _, filename := range []string{"/tmp/gorm_logger_writer.go", "/workspace/pkg/database/gormkit/logger.go"} {
|
||||||
if err := core.Write(zapcore.Entry{Level: zapcore.ErrorLevel, Message: "database failed", Caller: zapcore.EntryCaller{Defined: true, File: filename, Line: 10}}, nil); err != nil {
|
if err := core.Write(zapcore.Entry{Level: zapcore.ErrorLevel, Message: "database failed", Caller: zapcore.EntryCaller{Defined: true, File: filename, Line: 10}}, nil); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue