refactor wallet domain management
This commit is contained in:
@@ -10,3 +10,6 @@ Databases:
|
||||
Cache: redis://default:change-me@127.0.0.1:6379/0
|
||||
OnMicroService: false
|
||||
SecretKey: change-me-to-a-random-string
|
||||
|
||||
Wallet:
|
||||
ManualRechargeMaxAmount: 100000000
|
||||
|
||||
@@ -10,12 +10,18 @@ import (
|
||||
// Spec 是 Platform API 的运行配置。
|
||||
var Spec SrvConfig
|
||||
|
||||
// WalletConfig 保存钱包后台资金操作限制。
|
||||
type WalletConfig struct {
|
||||
ManualRechargeMaxAmount int64 `yaml:"ManualRechargeMaxAmount"`
|
||||
}
|
||||
|
||||
// SrvConfig 与 sample/server 配置结构保持一致。
|
||||
type SrvConfig struct {
|
||||
conf.Base `yaml:",inline"`
|
||||
Databases *conf.DBConf `yaml:"Databases"`
|
||||
Rpc map[string]conf.RpcConf `yaml:"Rpc"`
|
||||
Apm *conf.ApmConf `yaml:"APM"`
|
||||
Wallet WalletConfig `yaml:"Wallet"`
|
||||
}
|
||||
|
||||
// New 初始化 BSM 配置并校验服务监听地址。
|
||||
@@ -25,5 +31,8 @@ func New(srvKey string) {
|
||||
Spec.BindIP = conf.CheckIP(Spec.BindIP)
|
||||
Spec.Addr = net.JoinHostPort(Spec.BindIP, Spec.Port)
|
||||
conf.NotNil(Spec.Service, Spec.Cache)
|
||||
if Spec.Wallet.ManualRechargeMaxAmount <= 0 {
|
||||
panic("Wallet.ManualRechargeMaxAmount must be greater than zero")
|
||||
}
|
||||
conf.PrintInfo(Spec.Addr)
|
||||
}
|
||||
|
||||
@@ -70,6 +70,10 @@ var keywordSafeColumns = map[string]bool{
|
||||
"ticket_no": true, "category": true, "priority": true,
|
||||
"platform_role_code": true, "data_scope": true, "menu_code": true,
|
||||
"path": true, "resource_type": true,
|
||||
"owner_type": true, "owner_identity": true, "payment_no": true,
|
||||
"record_no": true, "request_no": true, "refund_no": true,
|
||||
"cash_no": true, "trade_no": true, "trade_type": true,
|
||||
"pay_channel": true, "payment_type": true,
|
||||
}
|
||||
|
||||
func applyKeywordFilter(ctx *gin.Context, query *gorm.DB, model any) *gorm.DB {
|
||||
|
||||
@@ -83,7 +83,7 @@ func ExpectedResources() []ResourceContract {
|
||||
resourceContract("finance", "fin_payment", Writable, "list"), resourceContract("finance", "fin_settlement", Writable, "list"), resourceContract("finance", "fin_reconciliation", Writable, "list"),
|
||||
resourceContract("content", "cms_content", Writable, "list"), resourceContract("customer_service", "cs_ticket", Writable, "list"),
|
||||
resourceContract("platform", "platfrom_account", Writable, "list"), resourceContract("platform", "platform_role", Writable, "list"), resourceContract("platform", "platform_menu", Writable, "tree"),
|
||||
resourceContract("wallet", "wallet", ReadOnly, "list"), resourceContract("wallet", "wallet_ledger", ReadOnly, "list"), resourceContract("wallet", "wallet_recharge", ReadOnly, "list"), resourceContract("wallet", "wallet_withdrawal", ReadOnly, "list"),
|
||||
resourceContract("wallet", "wallet_basic", ReadOnly, "list"), resourceContract("wallet", "wallet_bank", ReadOnly, "list"), resourceContract("wallet", "wallet_payment", ReadOnly, "list"), resourceContract("wallet", "wallet_record", ReadOnly, "list"), resourceContract("wallet", "wallet_refund", ReadOnly, "list"), resourceContract("wallet", "wallet_apply_cash", ReadOnly, "list"),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ func resourceContract(domain, name string, mode ResourceMode, pageKind string) R
|
||||
}
|
||||
|
||||
func resourcePath(domain, name string) string {
|
||||
if domain == "product" {
|
||||
if domain == "product" || domain == "wallet" {
|
||||
return "/" + name
|
||||
}
|
||||
switch name {
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
func TestExpectedResources(t *testing.T) {
|
||||
assertContract(t, ExpectedResources(), "gas", "gas_basic", Writable, "list")
|
||||
assertContract(t, ExpectedResources(), "ec", "ec_order_item", Writable, "list")
|
||||
assertContract(t, ExpectedResources(), "wallet", "wallet_ledger", ReadOnly, "list")
|
||||
assertContract(t, ExpectedResources(), "wallet", "wallet_record", ReadOnly, "list")
|
||||
assertContract(t, ExpectedResources(), "delivery", "delivery_track_point", ReadOnly, "list")
|
||||
assertContract(t, ExpectedResources(), "product", "product_info", Editable, "list")
|
||||
assertContract(t, ExpectedResources(), "product", "product_owner", AppendOnly, "list")
|
||||
|
||||
@@ -389,7 +389,10 @@ var relationIdentityModels = map[string]any{
|
||||
"delivery_track_id": &models.DeliveryTrack{},
|
||||
"platform_role_id": &models.PlatformRole{},
|
||||
"platform_menu_id": &models.PlatformMenu{},
|
||||
"wallet_id": &models.Wallet{},
|
||||
"wallet_basic_id": &models.WalletBasic{},
|
||||
"wallet_payment_id": &models.WalletPayment{},
|
||||
"wallet_bank_id": &models.WalletBank{},
|
||||
"related_record_id": &models.WalletRecord{},
|
||||
}
|
||||
|
||||
var relationIdentityKeys = map[string]string{
|
||||
|
||||
323
backend/api/internal/logic/platform/wallet.go
Normal file
323
backend/api/internal/logic/platform/wallet.go
Normal file
@@ -0,0 +1,323 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"math"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/errcode"
|
||||
"git.apinb.com/bsm-sdk/core/infra"
|
||||
"git.apinb.com/bsm-sdk/core/middleware"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/config"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/impl"
|
||||
"git.apinb.com/heqiapp/platforms/backend/api/internal/models"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
var walletOwnerModels = map[string]any{
|
||||
"user": &models.UserAccount{},
|
||||
"staff": &models.StaffAccount{},
|
||||
"delivery": &models.DeliveryBasic{},
|
||||
"gas": &models.GasBasic{},
|
||||
}
|
||||
|
||||
func ListWalletBasic(ctx *gin.Context) { listWalletPage[models.WalletBasic](ctx) }
|
||||
func GetWalletBasic(ctx *gin.Context) { getWalletByIdentity[models.WalletBasic](ctx) }
|
||||
func ListWalletBank(ctx *gin.Context) { listWalletPage[models.WalletBank](ctx) }
|
||||
func GetWalletBank(ctx *gin.Context) { getWalletByIdentity[models.WalletBank](ctx) }
|
||||
func ListWalletPayment(ctx *gin.Context) { listWalletPage[models.WalletPayment](ctx) }
|
||||
func GetWalletPayment(ctx *gin.Context) { getWalletByIdentity[models.WalletPayment](ctx) }
|
||||
func ListWalletRecord(ctx *gin.Context) { listWalletPage[models.WalletRecord](ctx) }
|
||||
func GetWalletRecord(ctx *gin.Context) { getWalletByIdentity[models.WalletRecord](ctx) }
|
||||
func ListWalletRefund(ctx *gin.Context) { listWalletPage[models.WalletRefund](ctx) }
|
||||
func GetWalletRefund(ctx *gin.Context) { getWalletByIdentity[models.WalletRefund](ctx) }
|
||||
func ListWalletApplyCash(ctx *gin.Context) { listWalletPage[models.WalletApplyCash](ctx) }
|
||||
func GetWalletApplyCash(ctx *gin.Context) { getWalletByIdentity[models.WalletApplyCash](ctx) }
|
||||
|
||||
func listWalletPage[T any](ctx *gin.Context) {
|
||||
page, size := pageSize(ctx)
|
||||
var list []T
|
||||
var total int64
|
||||
model := new(T)
|
||||
query := applyKeywordFilter(ctx, impl.DBService.Model(model), model)
|
||||
if err := query.Count(&total).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
if err := query.Order("created_at desc").Offset((page - 1) * size).Limit(size).Find(&list).Error; err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := publicResourceResponse(list)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
protectWalletResponse(response, false)
|
||||
infra.Response.Success(ctx, gin.H{"total": total, "list": protectPreciseLocation(ctx, model, response)})
|
||||
}
|
||||
|
||||
func getWalletByIdentity[T any](ctx *gin.Context) {
|
||||
var data T
|
||||
if err := impl.DBService.Where("identity = ?", ctx.Param("identity")).First(&data).Error; err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := publicResourceResponse(data)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
protectWalletResponse(response, true)
|
||||
infra.Response.Success(ctx, protectPreciseLocation(ctx, new(T), response))
|
||||
}
|
||||
|
||||
func protectWalletResponse(value any, includeDetails bool) {
|
||||
switch data := value.(type) {
|
||||
case map[string]any:
|
||||
for _, key := range []string{"alipay_id", "alipay_name", "wxpay_id", "wxpay_name"} {
|
||||
if text, ok := data[key].(string); ok && text != "" {
|
||||
data[key] = maskWalletAccount(text)
|
||||
}
|
||||
}
|
||||
if last4, ok := data["card_no_last4"].(string); ok && last4 != "" {
|
||||
data["card_no_masked"] = "****" + last4
|
||||
delete(data, "card_no_last4")
|
||||
}
|
||||
if owner, ok := data["card_owner"].(string); ok && owner != "" {
|
||||
data["card_owner"] = maskPersonalNameValue(owner)
|
||||
}
|
||||
if !includeDetails {
|
||||
delete(data, "args")
|
||||
delete(data, "callback_msg")
|
||||
delete(data, "order_info")
|
||||
delete(data, "result")
|
||||
}
|
||||
for _, item := range data {
|
||||
protectWalletResponse(item, includeDetails)
|
||||
}
|
||||
case []any:
|
||||
for _, item := range data {
|
||||
protectWalletResponse(item, includeDetails)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func maskWalletAccount(value string) string {
|
||||
runes := []rune(value)
|
||||
if len(runes) <= 2 {
|
||||
return strings.Repeat("*", len(runes))
|
||||
}
|
||||
return string(runes[0]) + strings.Repeat("*", len(runes)-2) + string(runes[len(runes)-1])
|
||||
}
|
||||
|
||||
func GetOrCreateOwnerWallet(ctx *gin.Context) {
|
||||
ownerType := strings.ToLower(strings.TrimSpace(ctx.Param("owner_type")))
|
||||
ownerIdentity := strings.TrimSpace(ctx.Param("owner_identity"))
|
||||
ownerID, err := resolveWalletOwner(ownerType, ownerIdentity)
|
||||
if err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
var wallet models.WalletBasic
|
||||
err = impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
err := tx.Where("owner_type = ? AND owner_identity = ?", ownerType, ownerIdentity).First(&wallet).Error
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
candidate := models.WalletBasic{
|
||||
Entity: models.Entity{Identity: models.NewIdentity(), Status: "enabled", Version: 1},
|
||||
OwnerType: ownerType, OwnerID: ownerID, OwnerIdentity: ownerIdentity,
|
||||
}
|
||||
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&candidate).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Where("owner_type = ? AND owner_identity = ?", ownerType, ownerIdentity).First(&wallet).Error
|
||||
})
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := publicResourceResponse(wallet)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, protectPreciseLocation(ctx, &models.WalletBasic{}, response))
|
||||
}
|
||||
|
||||
func resolveWalletOwner(ownerType, ownerIdentity string) (uint64, error) {
|
||||
if ownerType == "platform" {
|
||||
if ownerIdentity != "heqi" {
|
||||
return 0, errors.New("invalid platform owner")
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
model := walletOwnerModels[ownerType]
|
||||
if model == nil || ownerIdentity == "" {
|
||||
return 0, errors.New("invalid wallet owner")
|
||||
}
|
||||
return resolveIdentityID(model, ownerIdentity, true)
|
||||
}
|
||||
|
||||
func UpdateWalletBasicStatus(ctx *gin.Context) {
|
||||
var request struct {
|
||||
Status string `json:"status" binding:"required"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || (request.Status != "enabled" && request.Status != "disabled") {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
updateAllowedByIdentity(ctx, &models.WalletBasic{}, gin.H{"status": request.Status}, []string{"status"})
|
||||
}
|
||||
|
||||
func RechargeWalletBasic(ctx *gin.Context) {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
RequestNo string `json:"request_no" binding:"required,max=128"`
|
||||
Amount int64 `json:"amount" binding:"required"`
|
||||
Withdrawable bool `json:"withdrawable"`
|
||||
Reason string `json:"reason" binding:"required,max=1000"`
|
||||
Remark string `json:"remark" binding:"max=2000"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil ||
|
||||
request.Amount <= 0 || request.Amount > config.Spec.Wallet.ManualRechargeMaxAmount ||
|
||||
strings.TrimSpace(request.RequestNo) == "" || strings.TrimSpace(request.Reason) == "" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
operatorIdentity, operatorName := walletOperator(ctx)
|
||||
var record models.WalletRecord
|
||||
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("request_no = ?", request.RequestNo).First(&record).Error; err == nil {
|
||||
return nil
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
var wallet models.WalletBasic
|
||||
if err := tx.Where("identity = ?", ctx.Param("identity")).First(&wallet).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
updates := map[string]any{"balance": gorm.Expr("balance + ?", request.Amount)}
|
||||
query := tx.Model(&models.WalletBasic{}).
|
||||
Where("id = ? AND status = ? AND balance <= ?", wallet.ID, "enabled", math.MaxInt64-request.Amount)
|
||||
if request.Withdrawable {
|
||||
updates["withdrawal_balance"] = gorm.Expr("withdrawal_balance + ?", request.Amount)
|
||||
query = query.Where("withdrawal_balance <= ?", math.MaxInt64-request.Amount)
|
||||
}
|
||||
result := query.Updates(updates)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return errors.New("wallet is disabled or balance overflow")
|
||||
}
|
||||
if err := tx.Where("id = ?", wallet.ID).First(&wallet).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
record = models.WalletRecord{
|
||||
Entity: models.Entity{Identity: models.NewIdentity(), Status: "posted", Version: 1},
|
||||
WalletBasicID: wallet.ID, RecordNo: models.NewIdentity(), RequestNo: request.RequestNo,
|
||||
Direction: "income", TradeType: "recharge", Amount: request.Amount,
|
||||
BalanceAfter: wallet.Balance, WithdrawalBalanceAfter: wallet.WithdrawalBalance,
|
||||
InTradeNo: request.RequestNo, PayChannel: "manual",
|
||||
OperatorIdentity: operatorIdentity, OperatorName: operatorName,
|
||||
Ymd: dateNumber(now, "20060102"), Ym: dateNumber(now, "200601"),
|
||||
Remark: strings.TrimSpace(request.Reason + " " + request.Remark),
|
||||
}
|
||||
return tx.Create(&record).Error
|
||||
})
|
||||
if err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
response, err := publicResourceResponse(record)
|
||||
if err != nil {
|
||||
infra.Response.Error(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, response)
|
||||
}
|
||||
|
||||
func ApproveWalletApplyCash(ctx *gin.Context) {
|
||||
reviewWalletApplyCash(ctx, "approved")
|
||||
}
|
||||
|
||||
func RejectWalletApplyCash(ctx *gin.Context) {
|
||||
reviewWalletApplyCash(ctx, "rejected")
|
||||
}
|
||||
|
||||
func reviewWalletApplyCash(ctx *gin.Context, targetStatus string) {
|
||||
if !requirePlatformRoot(ctx) {
|
||||
return
|
||||
}
|
||||
var request struct {
|
||||
Reason string `json:"reason" binding:"required,max=2000"`
|
||||
}
|
||||
if err := ctx.ShouldBindJSON(&request); err != nil || strings.TrimSpace(request.Reason) == "" {
|
||||
infra.Response.Error(ctx, errcode.ErrInvalidArgument)
|
||||
return
|
||||
}
|
||||
operatorIdentity, operatorName := walletOperator(ctx)
|
||||
err := impl.DBService.Transaction(func(tx *gorm.DB) error {
|
||||
var application models.WalletApplyCash
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("identity = ?", ctx.Param("identity")).First(&application).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if application.Status == targetStatus {
|
||||
return nil
|
||||
}
|
||||
if application.Status != "pending" {
|
||||
return errors.New("cash application is not pending")
|
||||
}
|
||||
now := time.Now()
|
||||
if targetStatus == "rejected" {
|
||||
result := tx.Model(&models.WalletBasic{}).
|
||||
Where("id = ? AND withdrawal_balance <= ?", application.WalletBasicID, math.MaxInt64-application.Amount).
|
||||
Update("withdrawal_balance", gorm.Expr("withdrawal_balance + ?", application.Amount))
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected != 1 {
|
||||
return errors.New("withdrawal balance overflow")
|
||||
}
|
||||
}
|
||||
return tx.Model(&application).Updates(map[string]any{
|
||||
"status": targetStatus, "reviewer_identity": operatorIdentity, "reviewer_name": operatorName,
|
||||
"reviewed_at": &now, "review_reason": request.Reason,
|
||||
}).Error
|
||||
})
|
||||
if err != nil {
|
||||
respondRecordError(ctx, err)
|
||||
return
|
||||
}
|
||||
infra.Response.Success(ctx, gin.H{"updated": true, "status": targetStatus})
|
||||
}
|
||||
|
||||
func walletOperator(ctx *gin.Context) (string, string) {
|
||||
claims, err := middleware.ParseAuth(ctx)
|
||||
if err != nil {
|
||||
return "", ""
|
||||
}
|
||||
var account models.PlatfromAccount
|
||||
if err := impl.DBService.Select("display_name").Where("identity = ?", claims.Identity).First(&account).Error; err != nil {
|
||||
return claims.Identity, ""
|
||||
}
|
||||
return claims.Identity, account.DisplayName
|
||||
}
|
||||
|
||||
func dateNumber(value time.Time, layout string) int32 {
|
||||
number, _ := strconv.ParseInt(value.Format(layout), 10, 32)
|
||||
return int32(number)
|
||||
}
|
||||
63
backend/api/internal/logic/platform/wallet_test.go
Normal file
63
backend/api/internal/logic/platform/wallet_test.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestResolveWalletOwnerAcceptsOnlyStablePlatformOwner(t *testing.T) {
|
||||
id, err := resolveWalletOwner("platform", "heqi")
|
||||
if err != nil || id != 0 {
|
||||
t.Fatalf("platform wallet owner = (%d, %v)", id, err)
|
||||
}
|
||||
if _, err := resolveWalletOwner("platform", "admin"); err == nil {
|
||||
t.Fatal("arbitrary platform owner was accepted")
|
||||
}
|
||||
if _, err := resolveWalletOwner("unknown", "identity"); err == nil {
|
||||
t.Fatal("unknown wallet owner type was accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDateNumberBuildsWalletRecordIndexes(t *testing.T) {
|
||||
value := time.Date(2026, time.July, 28, 10, 30, 0, 0, time.UTC)
|
||||
if got := dateNumber(value, "20060102"); got != 20260728 {
|
||||
t.Fatalf("ymd = %d", got)
|
||||
}
|
||||
if got := dateNumber(value, "200601"); got != 202607 {
|
||||
t.Fatalf("ym = %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtectWalletResponseMasksAccountsAndListDetails(t *testing.T) {
|
||||
value := map[string]any{
|
||||
"alipay_id": "account@example.com",
|
||||
"card_no_last4": "1234",
|
||||
"card_owner": "张三",
|
||||
"args": "sensitive",
|
||||
"callback_msg": "sensitive",
|
||||
}
|
||||
protectWalletResponse(value, false)
|
||||
if value["alipay_id"] == "account@example.com" {
|
||||
t.Fatal("payment account was not masked")
|
||||
}
|
||||
if value["card_no_masked"] != "****1234" {
|
||||
t.Fatalf("masked card = %#v", value["card_no_masked"])
|
||||
}
|
||||
if _, exists := value["card_no_last4"]; exists {
|
||||
t.Fatal("raw card last four remains in response")
|
||||
}
|
||||
if _, exists := value["args"]; exists {
|
||||
t.Fatal("payment args remain in list response")
|
||||
}
|
||||
if _, exists := value["callback_msg"]; exists {
|
||||
t.Fatal("callback message remains in list response")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProtectWalletResponseRetainsDetailOnlyFields(t *testing.T) {
|
||||
value := map[string]any{"args": "detail", "callback_msg": "detail"}
|
||||
protectWalletResponse(value, true)
|
||||
if value["args"] != "detail" || value["callback_msg"] != "detail" {
|
||||
t.Fatalf("detail fields were removed: %#v", value)
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// Wallet 对应 wallet,保存余额账户。
|
||||
type Wallet struct {
|
||||
Entity // 公共实体字段
|
||||
OwnerType string `gorm:"column:owner_type;type:varchar(32);not null" json:"owner_type"` // owner_type 业务字段
|
||||
OwnerID uint64 `gorm:"column:owner_id;not null;index" json:"owner_id"` // owner_id 业务字段
|
||||
BalanceAmount int64 `gorm:"column:balance_amount;not null;default:0" json:"balance_amount"` // balance_amount 业务字段
|
||||
FrozenAmount int64 `gorm:"column:frozen_amount;not null;default:0" json:"frozen_amount"` // frozen_amount 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&Wallet{}) }
|
||||
func (table *Wallet) TableName() string { return "wallet" }
|
||||
30
backend/api/internal/models/wallet_apply_cash.go
Normal file
30
backend/api/internal/models/wallet_apply_cash.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// WalletApplyCash 对应 wallet_apply_cash,保存提现申请及审核结果。
|
||||
type WalletApplyCash struct {
|
||||
Entity // 公共实体字段
|
||||
WalletBasicID uint64 `gorm:"column:wallet_basic_id;not null;index" json:"wallet_basic_id"` // 钱包自增主键
|
||||
WalletBankID uint64 `gorm:"column:wallet_bank_id;not null;default:0;index" json:"wallet_bank_id"` // 银行卡自增主键
|
||||
CashNo string `gorm:"column:cash_no;type:varchar(64);not null;uniqueIndex" json:"cash_no"` // 内部提现单号
|
||||
RequestNo string `gorm:"column:request_no;type:varchar(128);not null;uniqueIndex" json:"request_no"` // 申请幂等号
|
||||
Amount int64 `gorm:"column:amount;not null;check:amount > 0" json:"amount"` // 提现金额,单位分
|
||||
Fee int64 `gorm:"column:fee;not null;default:0;check:fee >= 0" json:"fee"` // 提现手续费,单位分
|
||||
Channel string `gorm:"column:channel;type:varchar(32);not null" json:"channel"` // 提现渠道
|
||||
TradeNo string `gorm:"column:trade_no;type:varchar(128);not null;default:'';index" json:"trade_no"` // 第三方提现流水号
|
||||
Remark string `gorm:"column:remark;type:text;not null;default:''" json:"remark"` // 申请备注
|
||||
CallbackMsg string `gorm:"column:callback_msg;type:text;not null;default:''" json:"callback_msg"` // 提现回调信息
|
||||
ReviewerIdentity string `gorm:"column:reviewer_identity;type:varchar(36);not null;default:'';index" json:"reviewer_identity"` // 审核人业务标识
|
||||
ReviewerName string `gorm:"column:reviewer_name;type:varchar(64);not null;default:''" json:"reviewer_name"` // 审核人姓名快照
|
||||
ReviewedAt *time.Time `gorm:"column:reviewed_at;type:timestamptz" json:"reviewed_at"` // 审核时间
|
||||
ReviewReason string `gorm:"column:review_reason;type:text;not null;default:''" json:"review_reason"` // 审核原因
|
||||
CompletedAt *time.Time `gorm:"column:completed_at;type:timestamptz" json:"completed_at"` // 完成时间
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&WalletApplyCash{}) }
|
||||
func (table *WalletApplyCash) TableName() string { return "wallet_apply_cash" }
|
||||
22
backend/api/internal/models/wallet_bank.go
Normal file
22
backend/api/internal/models/wallet_bank.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// WalletBank 对应 wallet_bank,保存钱包绑定的银行卡密文档案。
|
||||
type WalletBank struct {
|
||||
Entity // 公共实体字段
|
||||
WalletBasicID uint64 `gorm:"column:wallet_basic_id;not null;index;uniqueIndex:idx_wallet_card" json:"wallet_basic_id"` // 钱包自增主键
|
||||
CardNoCiphertext string `gorm:"column:card_no_ciphertext;type:text;not null" json:"-"` // 加密银行卡号
|
||||
CardFingerprint string `gorm:"column:card_fingerprint;type:varchar(64);not null;uniqueIndex:idx_wallet_card" json:"-"` // 银行卡号不可逆指纹
|
||||
CardNoLast4 string `gorm:"column:card_no_last4;type:varchar(4);not null;index" json:"card_no_last4"` // 银行卡号末四位
|
||||
BankName string `gorm:"column:bank_name;type:varchar(128);not null" json:"bank_name"` // 银行名称
|
||||
CardOwner string `gorm:"column:card_owner;type:varchar(128);not null" json:"card_owner"` // 持卡人姓名
|
||||
IDCardCiphertext string `gorm:"column:id_card_ciphertext;type:text;not null" json:"-"` // 加密身份证号
|
||||
PhoneCiphertext string `gorm:"column:phone_ciphertext;type:text;not null" json:"-"` // 加密银行预留手机号
|
||||
BindID string `gorm:"column:bind_id;type:varchar(128);not null;default:'';uniqueIndex:,where:bind_id <> ''" json:"bind_id"` // 支付渠道绑定标识
|
||||
BankType string `gorm:"column:bank_type;type:varchar(20);not null;default:''" json:"bank_type"` // 银行卡类型
|
||||
Bank string `gorm:"column:bank;type:varchar(64);not null;default:''" json:"bank"` // 所属银行编码
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&WalletBank{}) }
|
||||
func (table *WalletBank) TableName() string { return "wallet_bank" }
|
||||
21
backend/api/internal/models/wallet_basic.go
Normal file
21
backend/api/internal/models/wallet_basic.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// WalletBasic 对应 wallet_basic,保存多主体统一钱包账户。
|
||||
type WalletBasic struct {
|
||||
Entity // 公共实体字段
|
||||
OwnerType string `gorm:"column:owner_type;type:varchar(32);not null;uniqueIndex:idx_wallet_owner" json:"owner_type"` // 归属主体类型
|
||||
OwnerID uint64 `gorm:"column:owner_id;not null;default:0;index" json:"owner_id"` // 归属主体自增主键
|
||||
OwnerIdentity string `gorm:"column:owner_identity;type:varchar(36);not null;uniqueIndex:idx_wallet_owner" json:"owner_identity"` // 归属主体业务标识
|
||||
AlipayID string `gorm:"column:alipay_id;type:varchar(128);not null;default:''" json:"alipay_id"` // 支付宝账号
|
||||
AlipayName string `gorm:"column:alipay_name;type:varchar(128);not null;default:''" json:"alipay_name"` // 支付宝账户名
|
||||
WxpayID string `gorm:"column:wxpay_id;type:varchar(128);not null;default:''" json:"wxpay_id"` // 微信支付账号
|
||||
WxpayName string `gorm:"column:wxpay_name;type:varchar(128);not null;default:''" json:"wxpay_name"` // 微信支付账户名
|
||||
PayPasswordHash string `gorm:"column:pay_password_hash;type:varchar(255);not null;default:''" json:"-"` // 支付密码哈希
|
||||
Balance int64 `gorm:"column:balance;not null;default:0;check:balance >= 0" json:"balance"` // 钱包余额,单位分
|
||||
WithdrawalBalance int64 `gorm:"column:withdrawal_balance;not null;default:0;check:withdrawal_balance >= 0" json:"withdrawal_balance"` // 可提现余额,单位分
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&WalletBasic{}) }
|
||||
func (table *WalletBasic) TableName() string { return "wallet_basic" }
|
||||
@@ -1,16 +0,0 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// WalletLedger 对应 wallet_ledger,保存不可变资金流水。
|
||||
type WalletLedger struct {
|
||||
Entity // 公共实体字段
|
||||
WalletID uint64 `gorm:"column:wallet_id;not null;index" json:"wallet_id"` // wallet_id 业务字段
|
||||
Amount int64 `gorm:"column:amount;not null" json:"amount"` // amount 业务字段
|
||||
Direction string `gorm:"column:direction;type:varchar(16);not null" json:"direction"` // direction 业务字段
|
||||
BalanceAfter int64 `gorm:"column:balance_after;not null" json:"balance_after"` // balance_after 业务字段
|
||||
ReferenceIdentity string `gorm:"column:reference_identity;type:varchar(36);not null;default:'';index" json:"reference_identity"` // reference_identity 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&WalletLedger{}) }
|
||||
func (table *WalletLedger) TableName() string { return "wallet_ledger" }
|
||||
22
backend/api/internal/models/wallet_payment.go
Normal file
22
backend/api/internal/models/wallet_payment.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// WalletPayment 对应 wallet_payment,保存第三方或余额支付单。
|
||||
type WalletPayment struct {
|
||||
Entity // 公共实体字段
|
||||
WalletBasicID uint64 `gorm:"column:wallet_basic_id;not null;index" json:"wallet_basic_id"` // 钱包自增主键
|
||||
PaymentNo string `gorm:"column:payment_no;type:varchar(64);not null;uniqueIndex" json:"payment_no"` // 内部支付单号
|
||||
OrderNo string `gorm:"column:order_no;type:varchar(128);not null;index" json:"order_no"` // 业务订单号
|
||||
TradeNo string `gorm:"column:trade_no;type:varchar(128);not null;default:'';uniqueIndex:idx_wallet_payment_trade,where:trade_no <> ''" json:"trade_no"` // 第三方交易流水号
|
||||
PaymentType string `gorm:"column:payment_type;type:varchar(32);not null" json:"payment_type"` // 支付业务类型
|
||||
PayChannel string `gorm:"column:pay_channel;type:varchar(32);not null;uniqueIndex:idx_wallet_payment_trade,where:trade_no <> ''" json:"pay_channel"` // 支付渠道
|
||||
PayType string `gorm:"column:pay_type;type:varchar(64);not null;default:''" json:"pay_type"` // 渠道支付类型
|
||||
Amount int64 `gorm:"column:amount;not null;check:amount > 0" json:"amount"` // 支付金额,单位分
|
||||
Args string `gorm:"column:args;type:text;not null;default:''" json:"args"` // 支付参数
|
||||
Remark string `gorm:"column:remark;type:text;not null;default:''" json:"remark"` // 备注
|
||||
CallbackMsg string `gorm:"column:callback_msg;type:text;not null;default:''" json:"callback_msg"` // 支付回调信息
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&WalletPayment{}) }
|
||||
func (table *WalletPayment) TableName() string { return "wallet_payment" }
|
||||
@@ -1,14 +0,0 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// WalletRecharge 对应 wallet_recharge,保存充值记录。
|
||||
type WalletRecharge struct {
|
||||
Entity // 公共实体字段
|
||||
WalletID uint64 `gorm:"column:wallet_id;not null;index" json:"wallet_id"` // wallet_id 业务字段
|
||||
Amount int64 `gorm:"column:amount;not null" json:"amount"` // amount 业务字段
|
||||
Channel string `gorm:"column:channel;type:varchar(32);not null" json:"channel"` // channel 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&WalletRecharge{}) }
|
||||
func (table *WalletRecharge) TableName() string { return "wallet_recharge" }
|
||||
30
backend/api/internal/models/wallet_record.go
Normal file
30
backend/api/internal/models/wallet_record.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// WalletRecord 对应 wallet_record,保存不可变资金流水。
|
||||
type WalletRecord struct {
|
||||
Entity // 公共实体字段
|
||||
WalletBasicID uint64 `gorm:"column:wallet_basic_id;not null;index" json:"wallet_basic_id"` // 钱包自增主键
|
||||
RecordNo string `gorm:"column:record_no;type:varchar(64);not null;uniqueIndex" json:"record_no"` // 内部流水号
|
||||
RequestNo string `gorm:"column:request_no;type:varchar(128);not null;uniqueIndex" json:"request_no"` // 业务幂等号
|
||||
Direction string `gorm:"column:direction;type:varchar(16);not null" json:"direction"` // 收支方向
|
||||
TradeType string `gorm:"column:trade_type;type:varchar(32);not null;index" json:"trade_type"` // 交易类型
|
||||
Amount int64 `gorm:"column:amount;not null;check:amount > 0" json:"amount"` // 交易金额,单位分
|
||||
Fee int64 `gorm:"column:fee;not null;default:0;check:fee >= 0" json:"fee"` // 手续费,单位分
|
||||
BalanceAfter int64 `gorm:"column:balance_after;not null;check:balance_after >= 0" json:"balance_after"` // 交易后余额
|
||||
WithdrawalBalanceAfter int64 `gorm:"column:withdrawal_balance_after;not null;check:withdrawal_balance_after >= 0" json:"withdrawal_balance_after"` // 交易后可提现余额
|
||||
InTradeNo string `gorm:"column:in_trade_no;type:varchar(128);not null;default:'';index" json:"in_trade_no"` // 内部业务流水号
|
||||
OutTradeNo string `gorm:"column:out_trade_no;type:varchar(128);not null;default:'';index" json:"out_trade_no"` // 外部渠道流水号
|
||||
PayChannel string `gorm:"column:pay_channel;type:varchar(32);not null;default:''" json:"pay_channel"` // 支付渠道
|
||||
PayType string `gorm:"column:pay_type;type:varchar(64);not null;default:''" json:"pay_type"` // 渠道支付类型
|
||||
RelatedRecordID uint64 `gorm:"column:related_record_id;not null;default:0;index" json:"related_record_id"` // 冲正关联原流水自增主键
|
||||
OperatorIdentity string `gorm:"column:operator_identity;type:varchar(36);not null;default:'';index" json:"operator_identity"` // 操作者业务标识
|
||||
OperatorName string `gorm:"column:operator_name;type:varchar(64);not null;default:''" json:"operator_name"` // 操作者姓名快照
|
||||
Ymd int32 `gorm:"column:ymd;not null;index" json:"ymd"` // 年月日索引
|
||||
Ym int32 `gorm:"column:ym;not null;index" json:"ym"` // 年月索引
|
||||
Remark string `gorm:"column:remark;type:text;not null;default:''" json:"remark"` // 交易备注
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&WalletRecord{}) }
|
||||
func (table *WalletRecord) TableName() string { return "wallet_record" }
|
||||
26
backend/api/internal/models/wallet_refund.go
Normal file
26
backend/api/internal/models/wallet_refund.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"git.apinb.com/bsm-sdk/core/database"
|
||||
)
|
||||
|
||||
// WalletRefund 对应 wallet_refund,保存支付退款结果。
|
||||
type WalletRefund struct {
|
||||
Entity // 公共实体字段
|
||||
WalletBasicID uint64 `gorm:"column:wallet_basic_id;not null;index" json:"wallet_basic_id"` // 钱包自增主键
|
||||
WalletPaymentID uint64 `gorm:"column:wallet_payment_id;not null;index" json:"wallet_payment_id"` // 原支付记录自增主键
|
||||
RefundNo string `gorm:"column:refund_no;type:varchar(64);not null;uniqueIndex" json:"refund_no"` // 内部退款单号
|
||||
OrderIdentity string `gorm:"column:order_identity;type:varchar(64);not null;index" json:"order_identity"` // 订单业务标识
|
||||
Amount int64 `gorm:"column:amount;not null;check:amount > 0" json:"amount"` // 退款金额,单位分
|
||||
Fee int64 `gorm:"column:fee;not null;default:0;check:fee >= 0" json:"fee"` // 退款手续费,单位分
|
||||
Reason string `gorm:"column:reason;type:text;not null;default:''" json:"reason"` // 退款原因
|
||||
OrderInfo string `gorm:"column:order_info;type:text;not null;default:''" json:"order_info"` // 订单信息快照
|
||||
Result string `gorm:"column:result;type:text;not null;default:''" json:"result"` // 第三方处理结果
|
||||
TradeNo string `gorm:"column:trade_no;type:varchar(128);not null;default:'';index" json:"trade_no"` // 第三方退款流水号
|
||||
CompletedAt *time.Time `gorm:"column:completed_at;type:timestamptz" json:"completed_at"` // 完成时间
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&WalletRefund{}) }
|
||||
func (table *WalletRefund) TableName() string { return "wallet_refund" }
|
||||
@@ -1,14 +0,0 @@
|
||||
package models
|
||||
|
||||
import "git.apinb.com/bsm-sdk/core/database"
|
||||
|
||||
// WalletWithdrawal 对应 wallet_withdrawal,保存提现记录。
|
||||
type WalletWithdrawal struct {
|
||||
Entity // 公共实体字段
|
||||
WalletID uint64 `gorm:"column:wallet_id;not null;index" json:"wallet_id"` // wallet_id 业务字段
|
||||
Amount int64 `gorm:"column:amount;not null" json:"amount"` // amount 业务字段
|
||||
BankAccountMasked string `gorm:"column:bank_account_masked;type:varchar(128);not null;default:''" json:"bank_account_masked"` // bank_account_masked 业务字段
|
||||
}
|
||||
|
||||
func init() { database.AppendMigrate(&WalletWithdrawal{}) }
|
||||
func (table *WalletWithdrawal) TableName() string { return "wallet_withdrawal" }
|
||||
@@ -28,6 +28,7 @@ func RegisterPlatform(serviceKey string, engine *gin.Engine) {
|
||||
registerStaffRoute(protected)
|
||||
registerUserRoute(protected)
|
||||
registerProductRoute(protected)
|
||||
registerWalletRoute(protected)
|
||||
registerCommerceRoute(protected)
|
||||
registerFinanceRoute(protected)
|
||||
registerContentRoute(protected)
|
||||
@@ -87,6 +88,37 @@ func registerProductRoute(group *gin.RouterGroup) {
|
||||
owner.GET("/:identity", ownerGet)
|
||||
}
|
||||
|
||||
func registerWalletRoute(group *gin.RouterGroup) {
|
||||
basic := group.Group("/wallet_basic")
|
||||
basic.GET("", platform.ListWalletBasic)
|
||||
basic.GET("/:identity", platform.GetWalletBasic)
|
||||
basic.PATCH("/:identity/status", platform.UpdateWalletBasicStatus)
|
||||
basic.POST("/:identity/recharge", platform.RechargeWalletBasic)
|
||||
basic.GET("/owner/:owner_type/:owner_identity", platform.GetOrCreateOwnerWallet)
|
||||
|
||||
bank := group.Group("/wallet_bank")
|
||||
bank.GET("", platform.ListWalletBank)
|
||||
bank.GET("/:identity", platform.GetWalletBank)
|
||||
|
||||
payment := group.Group("/wallet_payment")
|
||||
payment.GET("", platform.ListWalletPayment)
|
||||
payment.GET("/:identity", platform.GetWalletPayment)
|
||||
|
||||
record := group.Group("/wallet_record")
|
||||
record.GET("", platform.ListWalletRecord)
|
||||
record.GET("/:identity", platform.GetWalletRecord)
|
||||
|
||||
refund := group.Group("/wallet_refund")
|
||||
refund.GET("", platform.ListWalletRefund)
|
||||
refund.GET("/:identity", platform.GetWalletRefund)
|
||||
|
||||
applyCash := group.Group("/wallet_apply_cash")
|
||||
applyCash.GET("", platform.ListWalletApplyCash)
|
||||
applyCash.GET("/:identity", platform.GetWalletApplyCash)
|
||||
applyCash.POST("/:identity/approve", platform.ApproveWalletApplyCash)
|
||||
applyCash.POST("/:identity/reject", platform.RejectWalletApplyCash)
|
||||
}
|
||||
|
||||
func registerCommerceRoute(group *gin.RouterGroup) {
|
||||
categoryRelations := []platform.ResourceRelation{optionalRelation("parent_identity", "parent_id", &models.EcCategory{})}
|
||||
_, categoryCreate, _, categoryUpdate := platform.ResourceHandlers(&models.EcCategory{}, []string{"name", "sort_no"}, []string{"name", "sort_no"}, categoryRelations...)
|
||||
@@ -140,10 +172,6 @@ func registerFinanceRoute(group *gin.RouterGroup) {
|
||||
registerWritableResource(group, "/finance/fin_settlement", settlementList, settlementCreate, settlementGet, settlementUpdate, &models.FinSettlement{})
|
||||
registerRestrictedWritableResource(group, "/finance/fin_reconciliation", &models.FinReconciliation{}, []string{"channel", "bill_date", "difference_amount"})
|
||||
|
||||
registerReadOnlyResource(group, "/wallet/wallet", &models.Wallet{})
|
||||
registerReadOnlyResource(group, "/wallet/wallet_ledger", &models.WalletLedger{})
|
||||
registerReadOnlyResource(group, "/wallet/wallet_recharge", &models.WalletRecharge{})
|
||||
registerReadOnlyResource(group, "/wallet/wallet_withdrawal", &models.WalletWithdrawal{})
|
||||
}
|
||||
|
||||
func registerContentRoute(group *gin.RouterGroup) {
|
||||
|
||||
@@ -166,7 +166,7 @@ func TestPlatformFinanceContentRoutesFollowTheirContracts(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, resource := range []string{
|
||||
"/wallet/wallet", "/wallet/wallet_ledger", "/wallet/wallet_recharge", "/wallet/wallet_withdrawal",
|
||||
"/wallet_basic", "/wallet_bank", "/wallet_payment", "/wallet_record", "/wallet_refund", "/wallet_apply_cash",
|
||||
} {
|
||||
path := "/heqi/platform/v1" + resource
|
||||
assertRouteMethods(t, routes, path, http.MethodGet)
|
||||
@@ -177,6 +177,19 @@ func TestPlatformFinanceContentRoutesFollowTheirContracts(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/wallet_basic/:identity/status", http.MethodPatch)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/wallet_basic/:identity/recharge", http.MethodPost)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/wallet_basic/owner/:owner_type/:owner_identity", http.MethodGet)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/wallet_apply_cash/:identity/approve", http.MethodPost)
|
||||
assertRouteMethods(t, routes, "/heqi/platform/v1/wallet_apply_cash/:identity/reject", http.MethodPost)
|
||||
for _, oldPath := range []string{
|
||||
"/heqi/platform/v1/wallet/wallet",
|
||||
"/heqi/platform/v1/wallet/wallet_ledger",
|
||||
"/heqi/platform/v1/wallet/wallet_recharge",
|
||||
"/heqi/platform/v1/wallet/wallet_withdrawal",
|
||||
} {
|
||||
assertNoRouteMethods(t, routes, oldPath, http.MethodGet, http.MethodPost, http.MethodPut, http.MethodPatch, http.MethodDelete)
|
||||
}
|
||||
|
||||
for _, resource := range []string{
|
||||
"/content/cnt_content", "/notification/ntf_template",
|
||||
|
||||
Reference in New Issue
Block a user