This commit is contained in:
parent
ceba8ce43d
commit
9d3bfd07ff
|
|
@ -44,6 +44,4 @@ ENV TZ=Asia/Shanghai
|
|||
|
||||
|
||||
CMD ["./server"]
|
||||
# 不被他人看法左右
|
||||
# 纸上谈兵是免费的
|
||||
# 《拿来》
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +1,56 @@
|
|||
package test
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"xy_sh/internal/biz"
|
||||
"xy_sh/internal/config"
|
||||
"xy_sh/internal/data/impl"
|
||||
"xy_sh/internal/entities"
|
||||
"xy_sh/internal/router"
|
||||
service2 "xy_sh/internal/service"
|
||||
"xy_sh/pkg/crypto"
|
||||
"xy_sh/utils"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
)
|
||||
|
||||
const URL = "http://127.0.0.1:8085"
|
||||
|
||||
func init() {
|
||||
crypto.InitCrypto("test_salt_12345", "test_key_16bytes")
|
||||
}
|
||||
|
||||
func setupTestApp() *fiber.App {
|
||||
app := fiber.New()
|
||||
router.SetupRoutes(app)
|
||||
cfgPath := flag.String("config", "../../config/config_test.yaml", "Path to configuration file")
|
||||
cfg, err := config.LoadConfig(*cfgPath)
|
||||
db, _ := utils.NewGormDb(cfg)
|
||||
orderImpl := impl.NewOrderImpl(db)
|
||||
logImpl := impl.NewLogImpl(db)
|
||||
ymt, err := ymtClient(cfg.Ymt)
|
||||
|
||||
if err != nil {
|
||||
panic("初始化YMT客户端失败: " + err.Error())
|
||||
}
|
||||
bizs := biz.NewOrderStore(orderImpl, ymt, cfg)
|
||||
service := service2.NewOrder(bizs, logImpl)
|
||||
|
||||
log.Infof("开始注册路由...") // 🔍 添加日志
|
||||
router.SetupRoutes(app, service)
|
||||
log.Infof("路由注册完成") // 🔍 添加日志
|
||||
// 🔍 打印所有路由
|
||||
for _, route := range app.GetRoutes() {
|
||||
log.Infof("Route: %s %s", route.Method, route.Path)
|
||||
}
|
||||
return app
|
||||
}
|
||||
|
||||
|
|
@ -54,7 +81,7 @@ func makeEncryptedRequest(t *testing.T, app *fiber.App, path string, bizData int
|
|||
req.Header.Set("timestamp", timestamp)
|
||||
req.Header.Set("sign", sign)
|
||||
|
||||
resp, err := app.Test(req)
|
||||
resp, err := app.Test(req, 50000)
|
||||
if err != nil {
|
||||
t.Fatalf("请求失败: %v", err)
|
||||
}
|
||||
|
|
@ -65,11 +92,24 @@ func makeEncryptedRequest(t *testing.T, app *fiber.App, path string, bizData int
|
|||
return resp, body
|
||||
}
|
||||
|
||||
func TestTestRoute(t *testing.T) {
|
||||
app := setupTestApp()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/test", nil)
|
||||
resp, err := app.Test(req)
|
||||
if err != nil {
|
||||
t.Fatalf("请求失败: %v", err)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
t.Logf("状态码: %d, 响应: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
func TestCreateOrder(t *testing.T) {
|
||||
app := setupTestApp()
|
||||
|
||||
bizData := entities.OrderRequest{
|
||||
ActCode: "ACT001",
|
||||
ActCode: "ACT007",
|
||||
GoodsCode: "123456",
|
||||
ActOrderNum: "TEST_ORDER_001",
|
||||
Account: "19912345678",
|
||||
|
|
@ -97,38 +137,9 @@ func TestCreateOrder(t *testing.T) {
|
|||
func TestQueryOrder(t *testing.T) {
|
||||
app := setupTestApp()
|
||||
|
||||
createBizData := entities.OrderRequest{
|
||||
ActCode: "ACT001",
|
||||
GoodsCode: "123456",
|
||||
ActOrderNum: "TEST_ORDER_002",
|
||||
Account: "19912345678",
|
||||
}
|
||||
|
||||
_, createBody := makeEncryptedRequest(t, app, "/api/v1/order/create", createBizData)
|
||||
|
||||
var createResp entities.CommonResponse
|
||||
if err := json.Unmarshal(createBody, &createResp); err != nil {
|
||||
t.Fatalf("创建订单响应解析失败: %v", err)
|
||||
}
|
||||
|
||||
if createResp.Code != entities.CodeSuccess {
|
||||
t.Fatalf("创建订单失败: %s", createResp.Msg)
|
||||
}
|
||||
|
||||
encryptedData := createResp.Data.(string)
|
||||
decryptedData, err := crypto.SM4CBCDecrypt(encryptedData)
|
||||
if err != nil {
|
||||
t.Fatalf("解密创建订单响应失败: %v", err)
|
||||
}
|
||||
|
||||
var orderResp entities.OrderResponseData
|
||||
if err := json.Unmarshal(decryptedData, &orderResp); err != nil {
|
||||
t.Fatalf("解析创建订单业务数据失败: %v", err)
|
||||
}
|
||||
|
||||
queryBizData := entities.QueryOrderRequest{
|
||||
ActCode: "ACT001",
|
||||
OrderNo: orderResp.OrderNo,
|
||||
//ActCode: "ACT007",
|
||||
OrderNo: "936312532119846913",
|
||||
}
|
||||
|
||||
resp, body := makeEncryptedRequest(t, app, "/api/v1/order/query", queryBizData)
|
||||
|
|
@ -238,11 +249,11 @@ func TestHealthCheck(t *testing.T) {
|
|||
app := setupTestApp()
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
resp, body := app.Test(req)
|
||||
resp, err := app.Test(req)
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("期望状态码200,实际: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
t.Logf("健康检查接口测试成功,响应: %s", string(body))
|
||||
}
|
||||
t.Logf("健康检查接口测试成功,响应: %s", err)
|
||||
}
|
||||
|
|
@ -1,18 +1,29 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"xy_sh/internal/biz"
|
||||
"xy_sh/internal/data/impl"
|
||||
service2 "xy_sh/internal/service"
|
||||
"xy_sh/utils"
|
||||
"xy_sh/ymt_v3"
|
||||
|
||||
"xy_sh/internal/config"
|
||||
"xy_sh/internal/router"
|
||||
"xy_sh/pkg/crypto"
|
||||
|
||||
"github.com/go-viper/mapstructure/v2"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.LoadConfig()
|
||||
|
||||
cfgPath := flag.String("config", "./config/config_test.yaml", "Path to configuration file")
|
||||
cfg, err := config.LoadConfig(*cfgPath)
|
||||
if err != nil {
|
||||
log.Fatalf("加载配置失败: %v", err)
|
||||
}
|
||||
crypto.InitCrypto(cfg.SM3Salt, cfg.SM4Key)
|
||||
|
||||
app := fiber.New(fiber.Config{
|
||||
|
|
@ -21,10 +32,31 @@ func main() {
|
|||
WriteTimeout: cfg.WriteTimeout,
|
||||
})
|
||||
|
||||
router.SetupRoutes(app)
|
||||
db, cleanup := utils.NewGormDb(cfg)
|
||||
defer cleanup()
|
||||
orderImpl := impl.NewOrderImpl(db)
|
||||
logImpl := impl.NewLogImpl(db)
|
||||
ymt, err := ymtClient(cfg.Ymt)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
bizs := biz.NewOrderStore(orderImpl, ymt, cfg)
|
||||
service := service2.NewOrder(bizs, logImpl)
|
||||
router.SetupRoutes(app, service)
|
||||
|
||||
log.Printf("服务启动,监听端口: %s", cfg.ServerPort)
|
||||
if err := app.Listen(":" + cfg.ServerPort); err != nil {
|
||||
log.Fatalf("服务启动失败: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ymtClient(cfg config.Ymt) (*ymt_v3.Client, error) {
|
||||
return ymt_v3.NewClient(&ymt_v3.ClientConfig{
|
||||
AppID: cfg.AppID,
|
||||
PrivateKey: cfg.PrivateKey,
|
||||
PublicKey: cfg.PublicKey,
|
||||
Key: cfg.Key,
|
||||
EncryptType: ymt_v3.EncryptType(cfg.EncryptType),
|
||||
Env: ymt_v3.Env(cfg.Env),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
# 服务器配置
|
||||
server:
|
||||
port: 8090
|
||||
host: "0.0.0.0"
|
||||
|
||||
ollama:
|
||||
base_url: "http://192.168.6.115:11434"
|
||||
model: "qwen3:8b"
|
||||
generate_model: "qwen3:8b"
|
||||
mapping_model: "qwen3:8b"
|
||||
# model: "qwen3-coder:480b-cloud"
|
||||
# generate_model: "qwen3-coder:480b-cloud"
|
||||
# mapping_model: "deepseek-v3.2:cloud"
|
||||
vl_model: "qwen2.5vl:3b"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
format: "json"
|
||||
|
||||
vllm:
|
||||
vl_model:
|
||||
base_url: "http://192.168.6.115:8001/v1"
|
||||
model: "qwen2.5-vl-3b-awq"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
text_model:
|
||||
base_url: "http://192.168.6.115:8002/v1"
|
||||
model: "qwen3-8b-fp8"
|
||||
timeout: "120s"
|
||||
level: "info"
|
||||
|
||||
coze:
|
||||
base_url: "https://api.coze.cn"
|
||||
|
||||
lsxd:
|
||||
# 统一登录
|
||||
login_url: "https://api.user.1688sup.com/v1/login/phone"
|
||||
phone: "ORlviZN7N06W2+WKLe76xg=="
|
||||
password: "V5Uh8C4bamEM6UQZh4TCeQ=="
|
||||
code: "456789"
|
||||
check_token_url: "https://api.user.1688sup.com/v1/user/welcome"
|
||||
|
||||
|
||||
sys:
|
||||
session_len: 6
|
||||
channel_pool_len: 100
|
||||
channel_pool_size: 32
|
||||
llm_pool_len: 5
|
||||
heartbeat_interval: 300
|
||||
key: report-api
|
||||
pollSize: 5 #连接池大小,不配置,或配置为0表示不启用连接池
|
||||
minIdleConns: 2 #最小空闲连接数
|
||||
maxIdleTime: 30 #每个连接最大空闲时间,如果超过了这个时间会被关闭
|
||||
tls: 30
|
||||
db:
|
||||
redis:
|
||||
host: 47.97.27.195:6379
|
||||
type: node
|
||||
pass: lansexiongdi@666
|
||||
key: ai_scheduler_prov
|
||||
pollSize: 5 #连接池大小,不配置,或配置为0表示不启用连接池
|
||||
minIdleConns: 2 #最小空闲连接数
|
||||
maxIdleTime: 30 #每个连接最大空闲时间,如果超过了这个时间会被关闭
|
||||
tls: 30
|
||||
db:
|
||||
db:
|
||||
driver: mysql
|
||||
source: root:SD###sdf323r343@tcp(121.199.38.107:3306)/sys_ai?charset=utf8mb4&parseTime=true&loc=Asia%2FShanghai
|
||||
mongo:
|
||||
source: mongodb://root:lsxd2026123@192.168.6.115:27017
|
||||
dataBase: ai_scheduler
|
||||
maxPoolSize: 100
|
||||
minPoolSize: 10
|
||||
maxConnIdleTime: 30
|
||||
connectTimeout: 10
|
||||
socketTimeout: 30
|
||||
oss:
|
||||
access_key: "LTAI5tGGZzjf3tvqWk8SQj2G"
|
||||
secret_key: "S0NKOAUaYWoK4EGSxrMFmYDzllhvpq"
|
||||
bucket: "attachment-public"
|
||||
domain: "https://attachment-public.oss-cn-hangzhou.aliyuncs.com"
|
||||
endpoint: "https://oss-cn-hangzhou.aliyuncs.com"
|
||||
|
||||
tools:
|
||||
zltxOrderDetail:
|
||||
enabled: true
|
||||
base_url: "https://revcl.1688sup.com/api/admin/direct/ai/%s"
|
||||
add_url: "https://revcl.1688sup.com/api/admin/direct/log/%s/%s"
|
||||
api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzU4MDkxOTU4LCJuYmYiOjE3NTgwOTAxNTgsImp0aSI6IjEiLCJQaG9uZSI6IjE4MDAwMDAwMDAwIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9DQVNISUVSLFZDTF9PUEVSQVRFLFZDTF9BRE1JTixWQ0xfQUFBLFZDTF9WQ0xfT1BFUkFULFZDTF9JTlZPSUNFLENSTV9BRE1JTixMSUFOTElBTl9BRE1JTixNQVJLRVRNQUcyX0FETUlOLFBIT05FQklMTF9BRE1JTixRSUFOWkhVX1NVUFBFUl9BRE0sTUFSS0VUSU5HU0FBU19TVVBFUkFETUlOLENBUkRfQ09ERSxDQVJEX1BST0NVUkVNRU5ULE1BUktFVElOR1NZU1RFTV9TVVBFUixTVEFUSVNUSUNBTFNZU1RFTV9BRE1JTixaTFRYX0FETUlOLFpMVFhfT1BFUkFURSIsIkRpbmdVc2VySWQiOiIxNjIwMjYxMjMwMjg5MzM4MzQifQ.Bjsx9f8yfcrV9EWxb0n6POwnXVOq9XPRD78JFZnnf1_VAVMN78W4W570SZL27PWuDnkD7E4oUg6RzeZwZgl7BZrNpNr-a-QpNC5qCptqrqXeNfVStmX7pxWA8GqnzI8ybkZgbhQ58Gje7DzdJtBq_8zte_LDaYhTYXdIc5EAG0AbCzAk22nPTl47nkMeHtmisXQVLEsdibl1hW3ViFJlXwfXvUrOENItmL1_mRYkggUB0MaTu2nHJOYM6PaOVGLHx-74eepnmK2rm6konFEb6ed-Ukc6gVR-nM9yWZaYLYNGNKJLwZoCX3tRuerq74n4kzQgWmUEJeaVI1yIGSw1zw"
|
||||
zltxProduct:
|
||||
enabled: true
|
||||
base_url: "https://revcl.1688sup.com/api/admin/oursProduct"
|
||||
add_url: "https://revcl.1688sup.com/api/admin/platformProduct/getProductsByOfficialProductId"
|
||||
api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzU2MTgyNTM1LCJuYmYiOjE3NTYxODA3MzUsImp0aSI6IjEiLCJQaG9uZSI6IjE4MDAwMDAwMDAwIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9DQVNISUVSLFZDTF9PUEVSQVRFLFZDTF9BRE1JTixWQ0xfQUFBLFZDTF9WQ0xfT1BFUkFULFZDTF9JTlZPSUNFLENSTV9BRE1JTixMSUFOTElBTl9BRE1JTixNQVJLRVRNQUcyX0FETUlOLFBIT05FQklMTF9BRE1JTixRSUFOWkhVX1NVUFBFUl9BRE0sTUFSS0VUSU5HU0FBU19TVVBFUkFETUlOLENBUkRfQ09ERSxDQVJEX1BST0NVUkVNRU5ULE1BUktFVElOR1NZU1RFTV9TVVBFUixTVEFUSVNUSUNBTFNZU1RFTV9BRE1JTixaTFRYX0FETUlOLFpMVFhfT1BFUkFURSIsIkRpbmdVc2VySWQiOiIxNjIwMjYxMjMwMjg5MzM4MzQifQ.N1xv1PYbcO8_jR5adaczc16YzGsr4z101gwEZdulkRaREBJNYTOnFrvRxTFx3RJTooXsqTqroE1MR84v_1WPX6BS6kKonA-kC1Jgot6yrt5rFWhGNGb2Cpr9rKIFCCQYmiGd3AUgDazEeaQ0_sodv3E-EXg9VfE1SX8nMcck9Yjnc8NCy7RTWaBIaSeOdZcEl-JfCD0S6GSx3oErp_hk-U9FKGwf60wAuDGTY1R0BP4BYpcEqS-C2LSnsSGyURi54Cuk5xH8r1WuF0Dm5bwAj5d7Hvs77-N_sUF-C5ONqyZJRAEhYLgcmN9RX_WQZfizdQJxizlTczdpzYfy-v-1eQ"
|
||||
zltxOrderStatistics:
|
||||
base_url: "https://revcl.1688sup.com/api/admin/direct/ai/search/"
|
||||
enabled: true
|
||||
api_key: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJ1c2VyQ2VudGVyIiwiZXhwIjoxNzU2MTgyNTM1LCJuYmYiOjE3NTYxODA3MzUsImp0aSI6IjEiLCJQaG9uZSI6IjE4MDAwMDAwMDAwIiwiVXNlck5hbWUiOiJsc3hkIiwiUmVhbE5hbWUiOiLotoXnuqfnrqHnkIblkZgiLCJBY2NvdW50VHlwZSI6MSwiR3JvdXBDb2RlcyI6IlZDTF9DQVNISUVSLFZDTF9PUEVSQVRFLFZDTF9BRE1JTixWQ0xfQUFBLFZDTF9WQ0xfT1BFUkFULFZDTF9JTlZPSUNFLENSTV9BRE1JTixMSUFOTElBTl9BRE1JTixNQVJLRVRNQUcyX0FETUlOLFBIT05FQklMTF9BRE1JTixRSUFOWkhVX1NVUFBFUl9BRE0sTUFSS0VUSU5HU0FBU19TVVBFUkFETUlOLENBUkRfQ09ERSxDQVJEX1BST0NVUkVNRU5ULE1BUktFVElOR1NZU1RFTV9TVVBFUixTVEFUSVNUSUNBTFNZU1RFTV9BRE1JTixaTFRYX0FETUlOLFpMVFhfT1BFUkFURSIsIkRpbmdVc2VySWQiOiIxNjIwMjYxMjMwMjg5MzM4MzQifQ.N1xv1PYbcO8_jR5adaczc16YzGsr4z101gwEZdulkRaREBJNYTOnFrvRxTFx3RJTooXsqTqroE1MR84v_1WPX6BS6kKonA-kC1Jgot6yrt5rFWhGNGb2Cpr9rKIFCCQYmiGd3AUgDazEeaQ0_sodv3E-EXg9VfE1SX8nMcck9Yjnc8NCy7RTWaBIaSeOdZcEl-JfCD0S6GSx3oErp_hk-U9FKGwf60wAuDGTY1R0BP4BYpcEqS-C2LSnsSGyURi54Cuk5xH8r1WuF0Dm5bwAj5d7Hvs77-N_sUF-C5ONqyZJRAEhYLgcmN9RX_WQZfizdQJxizlTczdpzYfy-v-1eQ"
|
||||
knowledge:
|
||||
base_url: "http://117.175.169.61:10000"
|
||||
enabled: true
|
||||
DingTalkBot:
|
||||
enabled: true
|
||||
api_key: "dingsbbntrkeiyazcfdg"
|
||||
api_secret: "ObqxwyR20r9rVNhju0sCPQyQA98_FZSc32W4vgxnGFH_b02HZr1BPCJsOAF816nu"
|
||||
zltxOrderAfterSaleSupplier:
|
||||
enabled: true
|
||||
base_url: "https://revcl.1688sup.com/api/admin/afterSales/directs"
|
||||
zltxOrderAfterSaleReseller:
|
||||
enabled: true
|
||||
base_url: "https://revcl.1688sup.com/api/admin/afterSales/reseller_pre_ai"
|
||||
zltxOrderAfterSaleResellerBatch:
|
||||
enabled: true
|
||||
base_url: "https://revcl.1688sup.com/api/admin/afterSales/reseller_pre_ai"
|
||||
weather:
|
||||
enabled: true
|
||||
base_url: "https://restapi.amap.com/v3/weather/weatherInfo"
|
||||
api_key: "12afbde5ab78cb7e575ff76bd0bdef2b"
|
||||
cozeExpress:
|
||||
enabled: true
|
||||
base_url: "https://api.coze.cn"
|
||||
api_key: "7582477438102552616"
|
||||
api_secret: "pat_eEN0BdLNDughEtABjJJRYTW71olvDU0qUbfQUeaPc2NnYWO8HeyNoui5aR9z0sSZ"
|
||||
cozeCompany:
|
||||
enabled: true
|
||||
base_url: "https://api.coze.cn"
|
||||
api_key: "7583905168607100978"
|
||||
api_secret: "pat_eEN0BdLNDughEtABjJJRYTW71olvDU0qUbfQUeaPc2NnYWO8HeyNoui5aR9z0sSZ"
|
||||
zltxResellerAuthProductToManagerAndDefaultLossReason:
|
||||
base_url: "https://revcl.1688sup.com/api/admin/reseller/resellerAuthProduct/getManagerAndDefaultLossReason"
|
||||
|
||||
# eino tool 配置
|
||||
eino_tools:
|
||||
# 货易通商品上传
|
||||
hytProductUpload:
|
||||
base_url: "https://hyt.86698.cn/admin_upload/api/v1/goods/supplier/batch/add/complete"
|
||||
add_url: "https://hyt.86698.cn/#/goods/goodsManage"
|
||||
# 货易通供应商查询
|
||||
hytSupplierSearch:
|
||||
base_url: "https://hyt.86698.cn/admin_upload/api/v1/supplier/list"
|
||||
# 货易通仓库查询
|
||||
hytWarehouseSearch:
|
||||
base_url: "https://hyt.86698.cn/admin_upload/api/v1/warehouse/list"
|
||||
# 货易通商品添加
|
||||
hytGoodsAdd:
|
||||
base_url: "https://hyt.86698.cn/admin_upload/api/v1/goods/add"
|
||||
add_url: "https://hyt.86698.cn/#/goods/goodsManage"
|
||||
# 货易通商品图片添加
|
||||
hytGoodsMediaAdd:
|
||||
base_url: "https://hyt.86698.cn/admin_upload/api/v1/media/add/batch"
|
||||
# 货易通商品分类添加
|
||||
hytGoodsCategoryAdd:
|
||||
base_url: "https://hyt.86698.cn/admin_upload/api/v1/good/category/relation/add"
|
||||
# 货易通商品分类查询
|
||||
hytGoodsCategorySearch:
|
||||
base_url: "https://hyt.86698.cn/admin_upload/api/v1/goods/category/list"
|
||||
# 货易通商品品牌查询
|
||||
hytGoodsBrandSearch:
|
||||
base_url: "https://hyt.86698.cn/admin_upload/api/v1/goods/brand/list"
|
||||
# == 电商充值系统 ==
|
||||
# 我们的商品统计
|
||||
rechargeStatisticsOursProduct:
|
||||
base_url: "http://admin.lanseds.cn/admin/statistics/oursProduct"
|
||||
# == 通用工具 ==
|
||||
# 表格转图片
|
||||
excel2pic:
|
||||
base_url: "http://192.168.6.115:8010/api/v1/convert"
|
||||
|
||||
dingtalk:
|
||||
api_key: "dingsbbntrkeiyazcfdg"
|
||||
api_secret: "ObqxwyR20r9rVNhju0sCPQyQA98_FZSc32W4vgxnGFH_b02HZr1BPCJsOAF816nu"
|
||||
table_demand:
|
||||
url: "https://alidocs.dingtalk.com/i/nodes/2Amq4vjg89RnYx9DTp66m2orW3kdP0wQ"
|
||||
base_id: "2Amq4vjg89RnYx9DTp66m2orW3kdP0wQ"
|
||||
sheet_id_or_name: "数据表"
|
||||
# 机器人群组
|
||||
bot_group_id:
|
||||
bbxt: 29
|
||||
|
||||
qywx:
|
||||
corp_id: "ww48151f694fb8ec67"
|
||||
app_secret: "uYqtdwdtdH4Uv_P4is2AChuGzBCoB6cQDyRvpbW0Vmk"
|
||||
token: "zJdukry6"
|
||||
aes_key: "4VLH47qRGUogc2d3QLWuUhvJlk8Y0YuRjXzeBquBq8B"
|
||||
init_account: "les.,FuZhongYun"
|
||||
chat_id_len: 16
|
||||
default_config_id: 1
|
||||
bot_group_id:
|
||||
bbxt: 37
|
||||
|
||||
default_prompt:
|
||||
img_recognize:
|
||||
system_prompt:
|
||||
'你是一个具备图像理解与用户意图分析能力的智能助手。当用户提供一张图片时,请完成以下任务:
|
||||
1. 关键信息提取:
|
||||
提取出图片中对用户可能有用的关键信息(例如金额、日期、标题、编号、联系信息、商品名称等)。
|
||||
若图片为文档类(如合同、发票、收据),请结构化输出关键字段(如客户名称、金额、开票日期等)。
|
||||
'
|
||||
user_prompt: '识别图片内容'
|
||||
|
||||
# 权限配置
|
||||
permissionConfig:
|
||||
permission_url: "https://api.user.1688sup.com/v1/menu/myCodes?systemId="
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
# config.yaml
|
||||
server_port: "5002"
|
||||
|
||||
# SM3 盐值
|
||||
s_m3_salt: "test_salt_12345"
|
||||
|
||||
# SM4 密钥
|
||||
sm4_key: "test_key_16bytes"
|
||||
|
||||
# 通知 URL
|
||||
notify_url: "https://your-notify-url.com/callback"
|
||||
|
||||
# 读写超时时间
|
||||
read_timeout: 30s
|
||||
write_timeout: 30s
|
||||
|
||||
# 亿美通(YMT)配置
|
||||
ymt:
|
||||
app_id: "fzy-v3"
|
||||
private_key: |
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEogIBAAKCAQEA+yK/Z/Vvxqfw4hGWRKO94Mo0Dt/65kEpDGgc51oDQ4N5V/LVtn7x+RCIQQn9tQp1apEx5dxGEI+AyxHRjwEXKAhejEG93rF3BmjnMG73L8T8ld3OGtm1WnzN6qeHnzhBDNTKz6IrqpQboqbJFB0zaC9nlPzKqDMBkldAcHbyzIhqF0uYUpVR2PqzAc/j/gVErsO4q37yiGKzUoOVP3HxNffQXxPmzD3/d9mAIKQZdCpf+0v/qsTzH/QvMz4G5duRAs59FiRUbZrb/9t1jsNmf8KtH3psDTRv/ESl8lgor/roSq+aTeqPZKgaSM6k3MypCUClZ/yh6lCz9ziqzSXZowIDAQABAoIBAAkLH0lnFTdaJNp/N6IRI21M2sMOXcKc5hWESkYqf6OWwG/iVr/TonMNnVp6OaCjV8cWo21bmUpPYJvvcFRt/Z97bawvUd8LFPYuIOUlmdEmjtH2ws1Eubsgc2nXzpJ1PCJyJzuC43+pBbW9u8/1nTxLOLeswr971rqmWbOzFNs4nBZUyVrUUz2LMBW+p0RTtMfnQRf92nGOWADl/y4/Ly1Q3OjNF1rHox5ou9xITmIx8Y+1aEbT4NAqGyfufOrLvodaLYF/spI+Oe3Gz6GVVvhRPoQ0J6C9/mfuqZcqVWQ4QcnMOmy9KAGOgZGZnkcdU/31MJQpGUuv4ECqwWtkX8ECgYEA+5SvmfCtis8d8hDPcn9fZWoVCA/dJLx03JG/iEQsUPqtJRxU7sMFAzK6ADHFx+ztBJi3X2e2fGWhw+1dXGFBT2pIv507TG3saTrLjlrA8iPKIcWfjupbJUK4tbTvLPaCpZNpKBnCgq0vpaq0SI3fjoHrMERIZDdKT179fI9I1F0CgYEA/4wPcddPQTHzktjXHP7CY6ZOQZydM2STazp5aWAdHnEXOkRDuZGTdtdc3fqQy53EU2VLu5Jxbnqn+IC1njrEonP/ko0sRyMssa9JdXqhM4dWZJO1IgdWWNWiVgH5aTlKe8SB3bSiwxf1FEfYa9lh60+qqQ+QEcpUETouzh3+hf8CgYBVDDes18MjGM9rxKkMcOjD9O+1MP+2aosrAY55N2qv2X5s/D3uFTl9kkl0xV6yLnMVyba75ui29viPro+QKkSU3z5GoJWqScLQ9BJaRm3Rra2oaxF8k9dKKlsc+lSco50Y8lNrPgIWgQuJesLFgEih+WOThpHeZx6U5GzXDe019QKBgHS6lZN6tkkheBFr21bzR/gcz0JJN8Vx+6TPYQYxURvGrMWAyS7KwIFYfqMnAV0BA9zUOHPFwOqmPHPW1x8f2RIbynI26jLUbmX7m6J+EYRoHZ5zmhmhIGATtcNzw2m9Mbi3WlrbWD2lg91vs/wPoBrMmTgAU97MfPohiZ+9M7YhAoGAMeJMoAW586q4u0G+0GsNTNfkHfR+1JI0gej9pL+Xw3iWu/RM0EpLM3LsgNq22D7AjyelqOPx7ICjg0vGpGp7ECrpM8QjEPtD9sXp02E6qiPU50L+hlIAaUyGWlWYI/lF6aFpvrPtTtbDx/3dK8miN/0fTtUcix3tQ0KLNdDYvaY=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
public_key: |
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBCgKCAQEA+yK/Z/Vvxqfw4hGWRKO94Mo0Dt/65kEpDGgc51oDQ4N5V/LVtn7x+RCIQQn9tQp1apEx5dxGEI+AyxHRjwEXKAhejEG93rF3BmjnMG73L8T8ld3OGtm1WnzN6qeHnzhBDNTKz6IrqpQboqbJFB0zaC9nlPzKqDMBkldAcHbyzIhqF0uYUpVR2PqzAc/j/gVErsO4q37yiGKzUoOVP3HxNffQXxPmzD3/d9mAIKQZdCpf+0v/qsTzH/QvMz4G5duRAs59FiRUbZrb/9t1jsNmf8KtH3psDTRv/ESl8lgor/roSq+aTeqPZKgaSM6k3MypCUClZ/yh6lCz9ziqzSXZowIDAQA=
|
||||
-----END PUBLIC KEY-----
|
||||
key: "40ec4208893105e82f9aff5f59639296"
|
||||
activity_no: "FZY001"
|
||||
encrypt_type: "aes"
|
||||
env: "test"
|
||||
|
||||
# 数据库配置
|
||||
db:
|
||||
driver: "mysql"
|
||||
source: "root:lcNHzXpGN0yoX^N@tcp(47.97.27.195:3306)/transfer?charset=utf8mb4&parseTime=True&loc=Local"
|
||||
maxIdle: 10
|
||||
maxOpen: 100
|
||||
maxLifetime: 3600
|
||||
isDebug: true
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
#!/bin/bash
|
||||
|
||||
# 使用方法:
|
||||
# ./genModel.sh usercenter user
|
||||
# ./genModel.sh usercenter user_auth
|
||||
# 再将./genModel下的文件剪切到对应服务的model目录里面,记得改package
|
||||
|
||||
|
||||
#生成的表名
|
||||
tables=$1
|
||||
#表生成的genmodel目录
|
||||
modeldir=./internal/data/model
|
||||
|
||||
# 数据库配置
|
||||
prefix=xy_sh_
|
||||
|
||||
|
||||
|
||||
gentool --dsn "root:lcNHzXpGN0yoX^N@tcp(47.97.27.195:3306)/transfer?charset=utf8mb4&parseTime=true" -outPath ${modeldir} -onlyModel -modelPkgName "model" -tables ${prefix}${tables}
|
||||
38
xy_sh/go.mod
38
xy_sh/go.mod
|
|
@ -3,6 +3,42 @@ module xy_sh
|
|||
go 1.26
|
||||
|
||||
require (
|
||||
gitea.cdlsxd.cn/self-tools/l_request v1.0.8
|
||||
github.com/go-kratos/kratos/v2 v2.9.2
|
||||
github.com/gofiber/fiber/v2 v2.52.0
|
||||
github.com/google/uuid v1.5.0
|
||||
github.com/google/wire v0.7.0
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/tjfoc/gmsm v1.4.1
|
||||
)
|
||||
gorm.io/driver/mysql v1.6.0
|
||||
gorm.io/gorm v1.31.2
|
||||
xorm.io/builder v0.3.13
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.0.5 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/klauspost/compress v1.17.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.15 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/rivo/uniseg v0.2.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasthttp v1.51.0 // indirect
|
||||
github.com/valyala/tcplisten v1.0.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,186 +1,199 @@
|
|||
package biz
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"io"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
"strings"
|
||||
"time"
|
||||
"xy_sh/internal/config"
|
||||
"xy_sh/internal/data/impl"
|
||||
"xy_sh/internal/data/model"
|
||||
"xy_sh/pkg"
|
||||
"xy_sh/pkg/crypto"
|
||||
"xy_sh/ymt_v3"
|
||||
|
||||
"gitea.cdlsxd.cn/self-tools/l_request"
|
||||
|
||||
"xy_sh/internal/entities"
|
||||
)
|
||||
|
||||
type OrderStore struct {
|
||||
mu sync.RWMutex
|
||||
orders map[string]*OrderInfo
|
||||
orders *impl.OrderImpl
|
||||
ymtClient *ymt_v3.Client
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
type OrderInfo struct {
|
||||
OrderNo string
|
||||
ActOrderNum string
|
||||
GoodsCode string
|
||||
ActCode string
|
||||
Account string
|
||||
Status int
|
||||
CouponNo string
|
||||
CouponCode string
|
||||
ExpireTime string
|
||||
CouponId string
|
||||
AppId string
|
||||
OpenId string
|
||||
CreateTime time.Time
|
||||
UpdateTime time.Time
|
||||
}
|
||||
|
||||
var store *OrderStore
|
||||
|
||||
func init() {
|
||||
store = &OrderStore{
|
||||
orders: make(map[string]*OrderInfo),
|
||||
func NewOrderStore(orders *impl.OrderImpl, ymtClient *ymt_v3.Client, cfg *config.Config) *OrderStore {
|
||||
return &OrderStore{
|
||||
orders: orders,
|
||||
ymtClient: ymtClient,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func CreateOrder(req *entities.OrderRequest) (*entities.OrderResponseData, error) {
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
|
||||
orderNo := generateOrderNo()
|
||||
|
||||
orderInfo := &OrderInfo{
|
||||
OrderNo: orderNo,
|
||||
ActOrderNum: req.ActOrderNum,
|
||||
GoodsCode: req.GoodsCode,
|
||||
ActCode: req.ActCode,
|
||||
Account: req.Account,
|
||||
Status: entities.StatusSuccess,
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
func (o *OrderStore) CreateOrder(ctx context.Context, req *entities.OrderRequest) (*entities.OrderResponseData, error) {
|
||||
var orderInfo model.XyShOrder
|
||||
err := o.orders.GetByKey(ctx, "act_order_num", req.ActOrderNum, &orderInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if orderInfo.ID == 0 {
|
||||
orderInfo = model.XyShOrder{
|
||||
ActOrderNum: req.ActOrderNum,
|
||||
GoodsCode: req.GoodsCode,
|
||||
ActCode: req.ActCode,
|
||||
Account: req.Account,
|
||||
Status: entities.StatusSuccess,
|
||||
CallBackURL: req.CallbackUrl,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
orderInfo.CouponNo = generateCouponNo()
|
||||
orderInfo.CouponCode = orderInfo.CouponNo
|
||||
orderInfo.ExpireTime = time.Now().AddDate(1, 0, 0).Format("2006-01-02 15:04:05")
|
||||
|
||||
store.orders[orderNo] = orderInfo
|
||||
selfPublicIP, err := pkg.GetPublicIP()
|
||||
if err != nil {
|
||||
selfPublicIP = "120.55.12.245"
|
||||
}
|
||||
result, err := o.ymtClient.KeyOrder(&ymt_v3.KeyOrderRequest{
|
||||
OutBizNo: req.ActOrderNum,
|
||||
ActivityNo: o.cfg.Ymt.ActivityNo,
|
||||
Account: req.Account,
|
||||
NotifyURL: selfPublicIP + ":" + o.cfg.ServerPort + "/callback/notify",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
orderInfo.CouponNo = result.URL
|
||||
orderInfo.CouponCode = result.Key
|
||||
orderInfo.OrderNo = result.TradeNo
|
||||
orderInfo.Status = int32(result.Status)
|
||||
var validTime time.Time
|
||||
if len(result.ValidEndTime) != 0 {
|
||||
validTime, err = time.Parse(time.DateTime, result.ValidEndTime)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
orderInfo.ExpireTime = validTime
|
||||
if orderInfo.ID == 0 {
|
||||
err = o.orders.Add(ctx, &orderInfo)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &entities.OrderResponseData{
|
||||
OrderNo: orderNo,
|
||||
CouponNo: orderInfo.CouponNo,
|
||||
CouponCode: orderInfo.CouponCode,
|
||||
Status: orderInfo.Status,
|
||||
ExpireTime: orderInfo.ExpireTime,
|
||||
OrderNo: result.TradeNo,
|
||||
CouponNo: result.URL,
|
||||
CouponCode: result.Key,
|
||||
Status: o.getStatus(result.Status, result.ValidEndTime, err),
|
||||
ExpireTime: result.ValidEndTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func QueryOrder(req *entities.QueryOrderRequest) (*entities.QueryOrderResponseData, error) {
|
||||
store.mu.RLock()
|
||||
defer store.mu.RUnlock()
|
||||
|
||||
orderInfo, exists := store.orders[req.OrderNo]
|
||||
if !exists {
|
||||
return nil, errors.New("订单不存在")
|
||||
func (o *OrderStore) getStatus(ymtStatus uint32, ymtValidTime string, err error) int {
|
||||
if err != nil {
|
||||
return entities.StatusFailed
|
||||
}
|
||||
switch ymtStatus {
|
||||
case 2:
|
||||
return entities.StatusRedeemed
|
||||
case 3:
|
||||
return entities.StatusFailed
|
||||
default:
|
||||
t, err := time.Parse(time.DateTime, ymtValidTime)
|
||||
if err != nil {
|
||||
fmt.Printf("解析失败: %v\n", err)
|
||||
return entities.StatusFailed
|
||||
}
|
||||
if t.Unix() <= time.Now().Unix() {
|
||||
return entities.StatusExpired
|
||||
}
|
||||
return entities.StatusSuccess
|
||||
}
|
||||
}
|
||||
|
||||
func (o *OrderStore) QueryOrder(req *entities.QueryOrderRequest) (*entities.QueryOrderResponseData, error) {
|
||||
result, err := o.ymtClient.KeyQuery(&ymt_v3.KeyQueryRequest{
|
||||
TradeNo: req.OrderNo,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &entities.QueryOrderResponseData{
|
||||
OrderNo: orderInfo.OrderNo,
|
||||
Status: orderInfo.Status,
|
||||
Account: orderInfo.Account,
|
||||
CouponId: orderInfo.CouponId,
|
||||
OrderNo: result.TradeNo,
|
||||
Status: o.getStatus(result.Status, result.ValidEndTime, err),
|
||||
Account: result.Account,
|
||||
}
|
||||
|
||||
if orderInfo.CouponNo != "" {
|
||||
if result.URL != "" {
|
||||
resp.CardInfo = &entities.CardInfo{
|
||||
CouponNo: orderInfo.CouponNo,
|
||||
CouponCode: orderInfo.CouponCode,
|
||||
ExpireTime: orderInfo.ExpireTime,
|
||||
CouponNo: result.URL,
|
||||
CouponCode: result.Key,
|
||||
ExpireTime: result.ValidEndTime,
|
||||
}
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func WxRecharge(req *entities.WxRechargeRequest) (*entities.WxRechargeResponseData, error) {
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
|
||||
if orderInfo, exists := store.orders[req.OrderNo]; exists {
|
||||
return &entities.WxRechargeResponseData{
|
||||
OrderNo: orderInfo.OrderNo,
|
||||
Status: orderInfo.Status,
|
||||
CouponId: orderInfo.CouponId,
|
||||
}, nil
|
||||
func (o *OrderStore) HandleCallback(req *entities.CallBack) error {
|
||||
callBackData := &entities.CallbackRequest{
|
||||
OrderNo: req.Data.TradeNo,
|
||||
ActOrderNum: req.Data.OutBizNo,
|
||||
Status: o.getStatus(req.Data.Status, req.Data.ValidEndTime, nil),
|
||||
Account: req.Data.Account,
|
||||
CardInfo: &entities.CardInfo{
|
||||
CouponNo: req.Data.Url,
|
||||
CouponCode: req.Data.Key,
|
||||
ExpireTime: req.Data.ValidEndTime,
|
||||
},
|
||||
}
|
||||
|
||||
orderNo := req.OrderNo
|
||||
|
||||
orderInfo := &OrderInfo{
|
||||
OrderNo: orderNo,
|
||||
ActOrderNum: req.ActOrderNum,
|
||||
GoodsCode: req.GoodsCode,
|
||||
ActCode: req.ActCode,
|
||||
AppId: req.AppId,
|
||||
OpenId: req.OpenId,
|
||||
Status: entities.StatusSuccess,
|
||||
CouponId: generateCouponId(),
|
||||
CreateTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
sign, jsonData, err := makeEncryptedRequest(callBackData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
store.orders[orderNo] = orderInfo
|
||||
|
||||
return &entities.WxRechargeResponseData{
|
||||
OrderNo: orderNo,
|
||||
Status: orderInfo.Status,
|
||||
CouponId: orderInfo.CouponId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func HandleCallback(req *entities.CallbackRequest) error {
|
||||
store.mu.Lock()
|
||||
defer store.mu.Unlock()
|
||||
|
||||
orderInfo, exists := store.orders[req.OrderNo]
|
||||
if !exists {
|
||||
orderInfo = &OrderInfo{
|
||||
OrderNo: req.OrderNo,
|
||||
ActOrderNum: req.ActOrderNum,
|
||||
CreateTime: time.Now(),
|
||||
}
|
||||
store.orders[req.OrderNo] = orderInfo
|
||||
requset := &l_request.Request{
|
||||
Method: http.MethodPost,
|
||||
Json: jsonData,
|
||||
Url: o.cfg.NotifyUrl,
|
||||
Headers: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
"timestamp": fmt.Sprintf("%d", time.Now().UnixMilli()),
|
||||
"sign": sign,
|
||||
},
|
||||
}
|
||||
|
||||
orderInfo.Status = req.Status
|
||||
orderInfo.Account = req.Account
|
||||
orderInfo.CouponId = req.CouponId
|
||||
orderInfo.UpdateTime = time.Now()
|
||||
|
||||
if req.CardInfo != nil {
|
||||
orderInfo.CouponNo = req.CardInfo.CouponNo
|
||||
orderInfo.CouponCode = req.CardInfo.CouponCode
|
||||
orderInfo.ExpireTime = req.CardInfo.ExpireTime
|
||||
res, err := requset.Send()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if strings.ToLower(res.Text) != "ok" {
|
||||
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateOrderNo() string {
|
||||
return "HM" + strconv.FormatInt(time.Now().UnixMilli(), 10) + randomString(16)
|
||||
}
|
||||
|
||||
func generateCouponNo() string {
|
||||
return randomString(16)
|
||||
}
|
||||
|
||||
func generateCouponId() string {
|
||||
return randomString(8)
|
||||
}
|
||||
|
||||
func randomString(n int) string {
|
||||
b := make([]byte, (n+1)/2)
|
||||
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
||||
panic(err)
|
||||
func makeEncryptedRequest(bizData interface{}) (string, map[string]interface{}, error) {
|
||||
bizJSON, err := json.Marshal(bizData)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("业务数据序列化失败: %v", err)
|
||||
}
|
||||
return hex.EncodeToString(b)[:n]
|
||||
}
|
||||
|
||||
encryptedData, err := crypto.SM4CBCEncrypt(bizJSON)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf("加密失败: %v", err)
|
||||
}
|
||||
|
||||
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
|
||||
sign := crypto.GenerateSign(timestamp, encryptedData)
|
||||
|
||||
reqBody := entities.EncryptedRequest{
|
||||
EncryptedData: encryptedData,
|
||||
}
|
||||
reqJSON, _ := pkg.StructToMap(reqBody)
|
||||
return sign, reqJSON, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
package biz
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
var ProviderSetBiz = wire.NewSet(
|
||||
NewOrderStore,
|
||||
)
|
||||
|
|
@ -1,35 +1,57 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"time"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
ServerPort string
|
||||
SM3Salt string
|
||||
SM4Key string
|
||||
ServerPort string `mapstructure:"server_port"`
|
||||
SM3Salt string `mapstructure:"s_m3_salt"`
|
||||
SM4Key string `mapstructure:"sm4_key"`
|
||||
NotifyUrl string `mapstructure:"notify_url"`
|
||||
Ymt Ymt `mapstructure:"ymt"`
|
||||
DB DB `mapstructure:"db"`
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
}
|
||||
|
||||
func LoadConfig() *Config {
|
||||
port := getEnv("SERVER_PORT", "8080")
|
||||
sm3Salt := getEnv("SM3_SALT", "default_salt_change_me")
|
||||
sm4Key := getEnv("SM4_KEY", "default_key_16by")
|
||||
|
||||
return &Config{
|
||||
ServerPort: port,
|
||||
SM3Salt: sm3Salt,
|
||||
SM4Key: sm4Key,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
}
|
||||
type Ymt struct {
|
||||
AppID string `mapstructure:"app_id"`
|
||||
PrivateKey string `mapstructure:"private_key"`
|
||||
PublicKey string `mapstructure:"public_key"`
|
||||
Key string `mapstructure:"key"`
|
||||
ActivityNo string `mapstructure:"activity_no"`
|
||||
EncryptType string `mapstructure:"encrypt_type"`
|
||||
Env string `mapstructure:"env"`
|
||||
}
|
||||
|
||||
func getEnv(key, defaultValue string) string {
|
||||
if value := os.Getenv(key); value != "" {
|
||||
return value
|
||||
type DB struct {
|
||||
Driver string `mapstructure:"driver"`
|
||||
Source string `mapstructure:"source"`
|
||||
MaxIdle int32 `mapstructure:"maxIdle"`
|
||||
MaxOpen int32 `mapstructure:"maxOpen"`
|
||||
MaxLifetime int32 `mapstructure:"maxLifetime"`
|
||||
IsDebug bool `mapstructure:"isDebug"`
|
||||
}
|
||||
|
||||
func LoadConfig(configPath string) (*Config, error) {
|
||||
viper.SetConfigFile(configPath)
|
||||
viper.SetConfigType("yaml")
|
||||
|
||||
// 读取配置文件
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
// 解析配置
|
||||
var bc Config
|
||||
if err := viper.Unmarshal(&bc); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
return &bc, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"xy_sh/internal/data/model"
|
||||
"xy_sh/tmpl/dataTemp"
|
||||
"xy_sh/utils"
|
||||
)
|
||||
|
||||
type LogImpl struct {
|
||||
dataTemp.DataTemp
|
||||
db *utils.Db
|
||||
}
|
||||
|
||||
func NewLogImpl(db *utils.Db) *LogImpl {
|
||||
return &LogImpl{
|
||||
DataTemp: *dataTemp.NewDataTemp(db, new(model.XyShLog)),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *LogImpl) PrimaryKey() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
func (m *LogImpl) GetTemp() *dataTemp.DataTemp {
|
||||
return &m.DataTemp
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
package impl
|
||||
|
||||
import (
|
||||
"xy_sh/internal/data/model"
|
||||
"xy_sh/tmpl/dataTemp"
|
||||
"xy_sh/utils"
|
||||
)
|
||||
|
||||
type OrderImpl struct {
|
||||
dataTemp.DataTemp
|
||||
db *utils.Db
|
||||
}
|
||||
|
||||
func NewOrderImpl(db *utils.Db) *OrderImpl {
|
||||
return &OrderImpl{
|
||||
DataTemp: *dataTemp.NewDataTemp(db, new(model.XyShOrder)),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *OrderImpl) PrimaryKey() string {
|
||||
return "id"
|
||||
}
|
||||
|
||||
func (m *OrderImpl) GetTemp() *dataTemp.DataTemp {
|
||||
return &m.DataTemp
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const TableNameXyShLog = "xy_sh_log"
|
||||
|
||||
// XyShLog mapped from table <xy_sh_log>
|
||||
type XyShLog struct {
|
||||
ID int64 `gorm:"column:id;primaryKey" json:"id"`
|
||||
Req string `gorm:"column:req;not null" json:"req"`
|
||||
Decrypt string `gorm:"column:decrypt;not null" json:"decrypt"`
|
||||
Resp string `gorm:"column:resp;not null" json:"resp"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||
}
|
||||
|
||||
// TableName XyShLog's table name
|
||||
func (*XyShLog) TableName() string {
|
||||
return TableNameXyShLog
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
// Code generated by gorm.io/gen. DO NOT EDIT.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const TableNameXyShOrder = "xy_sh_order"
|
||||
|
||||
// XyShOrder mapped from table <xy_sh_order>
|
||||
type XyShOrder struct {
|
||||
ID int64 `gorm:"column:id;primaryKey" json:"id"`
|
||||
OrderNo string `gorm:"column:order_no;not null;comment:订单编号" json:"order_no"` // 订单编号
|
||||
ActOrderNum string `gorm:"column:act_order_num;not null;comment:活动方订单号,唯一,行内活动订单号" json:"act_order_num"` // 活动方订单号,唯一,行内活动订单号
|
||||
GoodsCode string `gorm:"column:goods_code;not null;comment:供应商商品编号" json:"goods_code"` // 供应商商品编号
|
||||
ActCode string `gorm:"column:act_code;not null;comment:活动code,可约定为各供应商的项目编号和密钥的拼接加密字符串" json:"act_code"` // 活动code,可约定为各供应商的项目编号和密钥的拼接加密字符串
|
||||
Account string `gorm:"column:account;not null;comment:充值账号" json:"account"` // 充值账号
|
||||
CallBackURL string `gorm:"column:call_back_url;not null;comment:回调地址" json:"call_back_url"` // 回调地址
|
||||
CouponNo string `gorm:"column:coupon_no;not null;comment:卡号,卡券/短链类商品返回" json:"coupon_no"` // 卡号,卡券/短链类商品返回
|
||||
CouponCode string `gorm:"column:coupon_code;not null;comment:卡密,卡券类商品返回" json:"coupon_code"` // 卡密,卡券类商品返回
|
||||
Status int32 `gorm:"column:status;not null;comment:状态,-1发放中 0成功 1失败 2已核销 3已过期" json:"status"` // 状态,-1发放中 0成功 1失败 2已核销 3已过期
|
||||
ExpireTime time.Time `gorm:"column:expire_time" json:"expire_time"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
// TableName XyShOrder's table name
|
||||
func (*XyShOrder) TableName() string {
|
||||
return TableNameXyShOrder
|
||||
}
|
||||
|
|
@ -40,4 +40,17 @@ type CardInfo struct {
|
|||
CouponNo string `json:"couponNo"`
|
||||
CouponCode string `json:"couponCode"`
|
||||
ExpireTime string `json:"expireTime"`
|
||||
}
|
||||
}
|
||||
|
||||
type CallBack struct {
|
||||
Data struct {
|
||||
NotifyId string `json:"notify_id"`
|
||||
OutBizNo string `json:"out_biz_no"`
|
||||
TradeNo string `json:"trade_no"`
|
||||
Status uint32 `json:"status"`
|
||||
Account string `json:"account"`
|
||||
ValidEndTime string `json:"valid_end_time"`
|
||||
Key string `json:"key"`
|
||||
Url string `json:"url"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,14 +2,54 @@ package middleware
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"xy_sh/tmpl/errcode"
|
||||
|
||||
"xy_sh/internal/entities"
|
||||
"xy_sh/pkg/crypto"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
)
|
||||
|
||||
func SignVerifyMiddleware() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
c.Locals("decryptedData", "")
|
||||
timestamp := c.Get("timestamp")
|
||||
sign := c.Get("sign")
|
||||
|
||||
if timestamp == "" || sign == "" {
|
||||
return fmt.Errorf("缺少timestamp或sign请求头")
|
||||
}
|
||||
|
||||
body := c.Body()
|
||||
|
||||
var req entities.EncryptedRequest
|
||||
if err := json.Unmarshal(body, &req); err != nil {
|
||||
return fmt.Errorf("请求体格式错误")
|
||||
}
|
||||
|
||||
if req.EncryptedData == "" {
|
||||
return fmt.Errorf("缺少encryptedData参数")
|
||||
}
|
||||
|
||||
if !crypto.VerifySign(timestamp, req.EncryptedData, sign) {
|
||||
return fmt.Errorf("签名验证失败")
|
||||
}
|
||||
|
||||
decryptedData, err := crypto.SM4CBCDecrypt(req.EncryptedData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("数据解密失败: " + err.Error())
|
||||
}
|
||||
|
||||
c.Locals("decryptedData", string(decryptedData))
|
||||
c.Locals("timestamp", timestamp)
|
||||
|
||||
return c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func SignVerifyMiddleware2() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
timestamp := c.Get("timestamp")
|
||||
sign := c.Get("sign")
|
||||
|
|
@ -65,23 +105,44 @@ func SignVerifyMiddleware() fiber.Handler {
|
|||
}
|
||||
}
|
||||
|
||||
type Res struct {
|
||||
Code int `json:"code"`
|
||||
Data interface{} `json:"data"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func EncryptResponseMiddleware() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
err := c.Next()
|
||||
if c.Path() == "/api/v1/callback/notify" {
|
||||
return nil
|
||||
}
|
||||
var resp *Res
|
||||
// 如果有错误发生
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
body := c.Response().Body()
|
||||
if len(body) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var resp entities.CommonResponse
|
||||
if err := json.Unmarshal(body, &resp); err != nil {
|
||||
return nil
|
||||
// 返回自定义错误响应
|
||||
resp = &Res{
|
||||
Msg: err.Error(),
|
||||
Code: entities.CodeFailed,
|
||||
Data: nil,
|
||||
}
|
||||
} else {
|
||||
body := c.Response().Body()
|
||||
if len(body) == 0 {
|
||||
return nil
|
||||
}
|
||||
resp = &Res{
|
||||
Code: entities.CodeSuccess,
|
||||
Data: body,
|
||||
Msg: errcode.Success.Error(),
|
||||
}
|
||||
}
|
||||
|
||||
//var resp entities.CommonResponse
|
||||
//if err := json.Unmarshal(res, &resp); err != nil {
|
||||
// return nil
|
||||
//}
|
||||
var newBody []byte
|
||||
if resp.Data != nil {
|
||||
dataBytes, err := json.Marshal(resp.Data)
|
||||
if err != nil {
|
||||
|
|
@ -103,7 +164,7 @@ func EncryptResponseMiddleware() fiber.Handler {
|
|||
|
||||
resp.Data = encryptedData
|
||||
|
||||
newBody, err := json.Marshal(resp)
|
||||
newBody, err = json.Marshal(resp)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).JSON(entities.CommonResponse{
|
||||
Code: entities.CodeFailed,
|
||||
|
|
@ -112,9 +173,20 @@ func EncryptResponseMiddleware() fiber.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
c.Response().SetBody(newBody)
|
||||
}
|
||||
|
||||
return c.JSON(newBody)
|
||||
}
|
||||
}
|
||||
|
||||
func SetLogReq() fiber.Handler {
|
||||
return func(c *fiber.Ctx) error {
|
||||
err := c.Next()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("Request: ", string(c.Request().Header.Header()), " Body: ", string(c.Request().Body()))
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,81 @@
|
|||
package router
|
||||
|
||||
import (
|
||||
"xy_sh/internal/entities"
|
||||
"xy_sh/internal/middleware"
|
||||
"xy_sh/internal/service"
|
||||
"xy_sh/pkg/crypto"
|
||||
"xy_sh/tmpl/errcode"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
)
|
||||
|
||||
func SetupRoutes(app *fiber.App) {
|
||||
app.Get("/health", service.HealthHandler)
|
||||
app.Get("/", service.HealthHandler)
|
||||
|
||||
v1 := app.Group("/api/v1")
|
||||
func SetupRoutes(
|
||||
app *fiber.App,
|
||||
service *service.Order,
|
||||
) {
|
||||
|
||||
v1 := app.Group("/api/v1", middleware.SetLogReq())
|
||||
app.Get("/test", func(c *fiber.Ctx) error {
|
||||
return c.SendString("test ok")
|
||||
})
|
||||
registerResponse(v1)
|
||||
v1.Post("/order/create",
|
||||
middleware.SignVerifyMiddleware(),
|
||||
middleware.EncryptResponseMiddleware(),
|
||||
service.CreateOrderHandler,
|
||||
)
|
||||
|
||||
v1.Post("/order/query",
|
||||
middleware.SignVerifyMiddleware(),
|
||||
middleware.EncryptResponseMiddleware(),
|
||||
service.QueryOrderHandler,
|
||||
)
|
||||
|
||||
v1.Post("/wx/recharge",
|
||||
middleware.SignVerifyMiddleware(),
|
||||
middleware.EncryptResponseMiddleware(),
|
||||
service.WxRechargeHandler,
|
||||
)
|
||||
|
||||
v1.Post("/callback/notify",
|
||||
middleware.SignVerifyMiddleware(),
|
||||
service.CallbackHandler,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func registerResponse(router fiber.Router) {
|
||||
// 自定义返回
|
||||
router.Use(func(c *fiber.Ctx) error {
|
||||
err := c.Next()
|
||||
return registerCommon(c, err)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
func registerCommon(c *fiber.Ctx, err error) error {
|
||||
|
||||
log.Info("Response: ", string(c.Response().Body()), " Err: ", err)
|
||||
if c.Path() == "/api/v1/callback/notify" {
|
||||
return nil
|
||||
}
|
||||
// 如果有错误发生
|
||||
if err != nil {
|
||||
// 返回自定义错误响应
|
||||
return c.JSON(fiber.Map{
|
||||
"msg": err.Error(),
|
||||
"code": entities.CodeFailed,
|
||||
"data": nil,
|
||||
})
|
||||
}
|
||||
|
||||
var data string
|
||||
if len(c.Response().Body()) > 0 {
|
||||
data, err = crypto.SM4CBCEncrypt(c.Response().Body())
|
||||
if err != nil {
|
||||
return c.JSON(fiber.Map{
|
||||
"msg": err.Error(),
|
||||
"code": entities.CodeFailed,
|
||||
"data": nil,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return c.JSON(fiber.Map{
|
||||
"data": data,
|
||||
"message": errcode.Success.Error(),
|
||||
"code": errcode.Success.Code(),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
package service
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
var ProviderSetService = wire.NewSet(
|
||||
NewOrder,
|
||||
)
|
||||
|
|
@ -2,6 +2,9 @@ package service
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"xy_sh/internal/data/impl"
|
||||
"xy_sh/pkg"
|
||||
|
||||
"xy_sh/internal/biz"
|
||||
"xy_sh/internal/entities"
|
||||
|
|
@ -9,63 +12,57 @@ import (
|
|||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
func CreateOrderHandler(c *fiber.Ctx) error {
|
||||
decryptedData := c.Locals("decryptedData").([]byte)
|
||||
type Order struct {
|
||||
biz *biz.OrderStore
|
||||
logImpl *impl.LogImpl
|
||||
}
|
||||
|
||||
func NewOrder(biz *biz.OrderStore, logImpl *impl.LogImpl) *Order {
|
||||
return &Order{
|
||||
biz: biz,
|
||||
logImpl: logImpl,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Order) CreateOrderHandler(c *fiber.Ctx) error {
|
||||
|
||||
decryptedData := c.Locals("decryptedData").([]byte)
|
||||
var req entities.OrderRequest
|
||||
if err := json.Unmarshal(decryptedData, &req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{
|
||||
Code: entities.CodeFailed,
|
||||
Msg: "业务参数解析失败: " + err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return fmt.Errorf("业务参数解析失败: %w", err)
|
||||
}
|
||||
|
||||
if req.ActCode == "" || req.GoodsCode == "" || req.ActOrderNum == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{
|
||||
Code: entities.CodeFailed,
|
||||
Msg: "缺少必填参数: actCode, goodsCode, actOrderNum",
|
||||
Data: nil,
|
||||
})
|
||||
return fmt.Errorf("缺少必填参数: actCode, goodsCode, actOrderNum")
|
||||
|
||||
}
|
||||
|
||||
result, err := biz.CreateOrder(&req)
|
||||
result, err := o.biz.CreateOrder(c.UserContext(), &req)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusOK).JSON(entities.CommonResponse{
|
||||
Code: entities.CodeFailed,
|
||||
Msg: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(entities.CommonResponse{
|
||||
Code: entities.CodeSuccess,
|
||||
Msg: "请求成功",
|
||||
Data: result,
|
||||
})
|
||||
return pkg.HandleResponse(c, result)
|
||||
}
|
||||
|
||||
func QueryOrderHandler(c *fiber.Ctx) error {
|
||||
decryptedData := c.Locals("decryptedData").([]byte)
|
||||
func (o *Order) QueryOrderHandler(c *fiber.Ctx) error {
|
||||
decryptedData := c.Locals("decryptedData").(string)
|
||||
if len(decryptedData) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var req entities.QueryOrderRequest
|
||||
if err := json.Unmarshal(decryptedData, &req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{
|
||||
Code: entities.CodeFailed,
|
||||
Msg: "业务参数解析失败: " + err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
if err := json.Unmarshal([]byte(decryptedData), &req); err != nil {
|
||||
return fmt.Errorf("业务参数解析失败: " + err.Error())
|
||||
|
||||
}
|
||||
|
||||
if req.ActCode == "" || req.OrderNo == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{
|
||||
Code: entities.CodeFailed,
|
||||
Msg: "缺少必填参数: actCode, orderNo",
|
||||
Data: nil,
|
||||
})
|
||||
if req.ActCode == "" && req.OrderNo == "" {
|
||||
return fmt.Errorf("缺少必填参数: actCode, orderNo")
|
||||
|
||||
}
|
||||
|
||||
result, err := biz.QueryOrder(&req)
|
||||
result, err := o.biz.QueryOrder(&req)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusOK).JSON(entities.CommonResponse{
|
||||
Code: entities.CodeFailed,
|
||||
|
|
@ -73,73 +70,16 @@ func QueryOrderHandler(c *fiber.Ctx) error {
|
|||
Data: nil,
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(entities.CommonResponse{
|
||||
Code: entities.CodeSuccess,
|
||||
Msg: "请求成功",
|
||||
Data: result,
|
||||
})
|
||||
return pkg.HandleResponse(c, result)
|
||||
}
|
||||
|
||||
func WxRechargeHandler(c *fiber.Ctx) error {
|
||||
decryptedData := c.Locals("decryptedData").([]byte)
|
||||
func (o *Order) CallbackHandler(c *fiber.Ctx) error {
|
||||
|
||||
var req entities.WxRechargeRequest
|
||||
if err := json.Unmarshal(decryptedData, &req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{
|
||||
Code: entities.CodeFailed,
|
||||
Msg: "业务参数解析失败: " + err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
}
|
||||
var req entities.CallBack
|
||||
|
||||
if req.ActCode == "" || req.OrderNo == "" || req.GoodsCode == "" || req.ActOrderNum == "" ||
|
||||
req.AppId == "" || req.OpenId == "" {
|
||||
return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{
|
||||
Code: entities.CodeFailed,
|
||||
Msg: "缺少必填参数: actCode, orderNo, goodsCode, actOrderNum, appId, openId",
|
||||
Data: nil,
|
||||
})
|
||||
}
|
||||
|
||||
result, err := biz.WxRecharge(&req)
|
||||
if err != nil {
|
||||
return c.Status(fiber.StatusOK).JSON(entities.CommonResponse{
|
||||
Code: entities.CodeFailed,
|
||||
Msg: err.Error(),
|
||||
Data: nil,
|
||||
})
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).JSON(entities.CommonResponse{
|
||||
Code: entities.CodeSuccess,
|
||||
Msg: "请求成功",
|
||||
Data: result,
|
||||
})
|
||||
}
|
||||
|
||||
func CallbackHandler(c *fiber.Ctx) error {
|
||||
decryptedData := c.Locals("decryptedData").([]byte)
|
||||
|
||||
var req entities.CallbackRequest
|
||||
if err := json.Unmarshal(decryptedData, &req); err != nil {
|
||||
return c.Status(fiber.StatusBadRequest).SendString("参数解析失败")
|
||||
}
|
||||
|
||||
if req.OrderNo == "" || req.ActOrderNum == "" {
|
||||
return c.Status(fiber.StatusBadRequest).SendString("缺少必填参数")
|
||||
}
|
||||
|
||||
if err := biz.HandleCallback(&req); err != nil {
|
||||
if err := o.biz.HandleCallback(&req); err != nil {
|
||||
return c.Status(fiber.StatusInternalServerError).SendString("处理失败")
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).SendString("ok")
|
||||
}
|
||||
|
||||
func HealthHandler(c *fiber.Ctx) error {
|
||||
return c.Status(fiber.StatusOK).JSON(fiber.Map{
|
||||
"status": "ok",
|
||||
"message": "服务运行正常",
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/gofiber/fiber/v2/log"
|
||||
)
|
||||
|
||||
// GetPublicIP 获取公网 IP
|
||||
func GetPublicIP() (string, error) {
|
||||
client := http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
|
||||
// 多个备用服务,提高可用性
|
||||
services := []struct {
|
||||
url string
|
||||
isJSON bool
|
||||
parseFn func([]byte) (string, error)
|
||||
}{
|
||||
{
|
||||
url: "https://api.ipify.org?format=json",
|
||||
isJSON: true,
|
||||
parseFn: func(body []byte) (string, error) {
|
||||
var result map[string]string
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ip, ok := result["ip"]; ok {
|
||||
return ip, nil
|
||||
}
|
||||
return "", fmt.Errorf("未找到 IP 字段")
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "https://ipinfo.io/ip",
|
||||
isJSON: false,
|
||||
parseFn: func(body []byte) (string, error) {
|
||||
return string(body), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "https://api.my-ip.io/ip",
|
||||
isJSON: false,
|
||||
parseFn: func(body []byte) (string, error) {
|
||||
return string(body), nil
|
||||
},
|
||||
},
|
||||
{
|
||||
url: "https://httpbin.org/ip",
|
||||
isJSON: true,
|
||||
parseFn: func(body []byte) (string, error) {
|
||||
var result map[string]string
|
||||
if err := json.Unmarshal(body, &result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if ip, ok := result["origin"]; ok {
|
||||
return ip, nil
|
||||
}
|
||||
return "", fmt.Errorf("未找到 IP 字段")
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
resp, err := client.Get(service.url)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ip, err := service.parseFn(body)
|
||||
if err == nil && ip != "" {
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("所有服务都无法获取公网 IP")
|
||||
}
|
||||
|
||||
func HandleResponse(c *fiber.Ctx, data interface{}) (err error) {
|
||||
if os.Getenv("env") == "unit_test" {
|
||||
log.Debug(data)
|
||||
}
|
||||
|
||||
switch data.(type) {
|
||||
case error:
|
||||
err = data.(error)
|
||||
case int, int32, int64, float32, float64, string, bool:
|
||||
c.Response().SetBody([]byte(fmt.Sprintf("%s", data)))
|
||||
case []byte:
|
||||
c.Response().SetBody(data.([]byte))
|
||||
default:
|
||||
dataByte, _ := json.Marshal(data)
|
||||
c.Response().SetBody(dataByte)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// StructToMap 将结构体转换为 map[string]any
|
||||
func StructToMap(v any) (map[string]any, error) {
|
||||
b, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var m map[string]any
|
||||
err = json.Unmarshal(b, &m)
|
||||
return m, err
|
||||
}
|
||||
|
||||
func StructToMapWithOutErr(v any) map[string]any {
|
||||
b, _ := json.Marshal(v)
|
||||
var m map[string]any
|
||||
_ = json.Unmarshal(b, &m)
|
||||
return m
|
||||
}
|
||||
|
|
@ -0,0 +1,266 @@
|
|||
package dataTemp
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"xy_sh/tmpl/errcode"
|
||||
"xy_sh/utils"
|
||||
|
||||
"github.com/go-kratos/kratos/v2/log"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"xorm.io/builder"
|
||||
)
|
||||
|
||||
type PrimaryKey struct {
|
||||
Id int `json:"id"`
|
||||
}
|
||||
|
||||
type GormDb struct {
|
||||
Client *gorm.DB
|
||||
}
|
||||
type contextTxKey struct{}
|
||||
|
||||
func (d *Db) DB(ctx context.Context) *gorm.DB {
|
||||
tx, ok := ctx.Value(contextTxKey{}).(*gorm.DB)
|
||||
if ok {
|
||||
return tx
|
||||
}
|
||||
return d.Db.Client
|
||||
}
|
||||
|
||||
func (t *Db) ExecTx(ctx context.Context, f func(ctx context.Context) error) error {
|
||||
return t.Db.Client.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
ctx = context.WithValue(ctx, contextTxKey{}, tx)
|
||||
return f(ctx)
|
||||
})
|
||||
}
|
||||
|
||||
type Db struct {
|
||||
Db *GormDb
|
||||
Log *log.Helper
|
||||
}
|
||||
|
||||
type DataTemp struct {
|
||||
Db *gorm.DB
|
||||
ModelType reflect.Type // 改为存储类型而不是实例
|
||||
modelName string // 可选的表名缓存
|
||||
}
|
||||
|
||||
func NewDataTemp(db *utils.Db, model interface{}) *DataTemp {
|
||||
// 获取模型的类型
|
||||
t := reflect.TypeOf(model)
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
return &DataTemp{
|
||||
Db: db.Client,
|
||||
ModelType: t,
|
||||
}
|
||||
}
|
||||
|
||||
func (k DataTemp) modelInstance() interface{} {
|
||||
return reflect.New(k.ModelType).Interface()
|
||||
}
|
||||
|
||||
func (k DataTemp) GetById(id int32) (data map[string]interface{}, err error) {
|
||||
err = k.Db.Model(k.modelInstance()).Where("id = ?", id).Find(&data).Error
|
||||
if data == nil {
|
||||
err = sql.ErrNoRows
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (k DataTemp) GetByStruct(ctx context.Context, search interface{}, data interface{}, orderBy string) (err error) {
|
||||
|
||||
err = k.Db.Model(k.modelInstance()).WithContext(ctx).Where(search).Find(&data).Error
|
||||
|
||||
if data == nil {
|
||||
err = sql.ErrNoRows
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (k DataTemp) SaveByStruct(search interface{}, data interface{}) (err error) {
|
||||
err = k.Db.Model(k.modelInstance()).Where(search).Save(&data).Error
|
||||
if data == nil {
|
||||
err = sql.ErrNoRows
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (k DataTemp) Add(ctx context.Context, data interface{}) (err error) {
|
||||
m := k.modelInstance()
|
||||
if err = k.Db.Model(m).WithContext(ctx).Create(data).Error; err != nil {
|
||||
return errcode.SqlErr(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (k DataTemp) AddWithData(data interface{}) (interface{}, error) {
|
||||
result := k.Db.Model(k.modelInstance()).Create(data)
|
||||
if result.Error != nil {
|
||||
return data, result.Error
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (k DataTemp) GetList(cond *builder.Cond, pageBoIn *ReqPageBo) (list []map[string]interface{}, pageBoOut *RespPageBo, err error) {
|
||||
var (
|
||||
query, _ = builder.ToBoundSQL(*cond)
|
||||
model = k.Db.Model(k.modelInstance()).Where(query)
|
||||
total int64
|
||||
)
|
||||
model.Count(&total)
|
||||
pageBoOut = pageBoOut.SetDataByReq(total, pageBoIn)
|
||||
model.Limit(pageBoIn.GetSize()).Offset(pageBoIn.GetOffset()).Order("updated_at desc").Find(&list)
|
||||
return
|
||||
}
|
||||
|
||||
func (k DataTemp) GetRange(ctx context.Context, cond *builder.Cond) (list []map[string]interface{}, err error) {
|
||||
var (
|
||||
query, _ = builder.ToBoundSQL(*cond)
|
||||
model = k.Db.Model(k.modelInstance()).Where(query)
|
||||
)
|
||||
err = model.WithContext(ctx).Find(&list).Error
|
||||
return list, err
|
||||
}
|
||||
|
||||
func (k DataTemp) GetRangeToMapStruct(ctx context.Context, cond *builder.Cond, data interface{}) (err error) {
|
||||
var (
|
||||
query, _ = builder.ToBoundSQL(*cond)
|
||||
model = k.Db.Model(k.modelInstance()).Where(query)
|
||||
)
|
||||
err = model.WithContext(ctx).Find(data).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func (k DataTemp) GetOneBySearch(cond *builder.Cond) (data map[string]interface{}, err error) {
|
||||
query, _ := builder.ToBoundSQL(*cond)
|
||||
if err = k.Db.Model(k.modelInstance()).Where(query).Limit(1).Find(&data).Error; err != nil {
|
||||
return nil, errcode.SqlErr(err)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (k DataTemp) Exist(ctx context.Context, cond *builder.Cond) (bool, error) {
|
||||
var data map[string]interface{}
|
||||
query, _ := builder.ToBoundSQL(*cond)
|
||||
err := k.Db.WithContext(ctx).Model(k.modelInstance()).Where(query).Limit(1).Find(&data).Error
|
||||
if err != nil || data != nil {
|
||||
return true, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (k DataTemp) GetListToStruct(ctx context.Context, cond *builder.Cond, pageBoIn *ReqPageBo, result interface{}, orderBy string) (pageBoOut *RespPageBo, err error) {
|
||||
// 参数验证
|
||||
if result == nil {
|
||||
return nil, fmt.Errorf("result cannot be nil")
|
||||
}
|
||||
|
||||
val := reflect.ValueOf(result)
|
||||
if val.Kind() != reflect.Ptr {
|
||||
return nil, fmt.Errorf("result must be a pointer")
|
||||
}
|
||||
|
||||
elem := val.Elem()
|
||||
if elem.Kind() != reflect.Slice {
|
||||
return nil, fmt.Errorf("result must be a pointer to slice")
|
||||
}
|
||||
|
||||
// 构建基础查询
|
||||
query, _ := builder.ToBoundSQL(*cond)
|
||||
|
||||
// 预编译 SQL 以提高性能
|
||||
// 使用 Table 指定表名,避免 GORM 的反射开销
|
||||
|
||||
db := k.Db.WithContext(ctx).Model(k.modelInstance()).Where(query)
|
||||
|
||||
// 获取总数(使用单独的计数查询,避免缓存影响)
|
||||
var total int64
|
||||
countDb := db
|
||||
if pageBoIn != nil {
|
||||
if err = countDb.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化分页响应
|
||||
pageBoOut = &RespPageBo{}
|
||||
pageBoOut = pageBoOut.SetDataByReq(total, pageBoIn)
|
||||
|
||||
// 如果没有数据,直接返回空切片
|
||||
if total == 0 && pageBoIn != nil {
|
||||
elem.Set(reflect.MakeSlice(elem.Type(), 0, 0))
|
||||
return pageBoOut, nil
|
||||
}
|
||||
|
||||
// 设置排序(使用索引字段提高性能)
|
||||
if orderBy == "" {
|
||||
orderBy = "updated_at desc"
|
||||
}
|
||||
|
||||
// 应用分页和排序,执行查询
|
||||
// 使用 Select 指定字段,避免查询所有字段(如果需要优化)
|
||||
baseQuery := db
|
||||
if pageBoIn != nil {
|
||||
baseQuery = db.Limit(pageBoIn.GetSize()).Offset(pageBoIn.GetOffset()).Order(orderBy)
|
||||
}
|
||||
if err = baseQuery.
|
||||
Order(orderBy).
|
||||
Find(result).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return pageBoOut, nil
|
||||
}
|
||||
|
||||
func (k DataTemp) UpdateByKey(ctx context.Context, key string, id interface{}, data interface{}) (err error) {
|
||||
if err = k.Db.WithContext(ctx).Model(k.modelInstance()).Where(fmt.Sprintf("%s = ?", key), id).Updates(data).Error; err != nil {
|
||||
return errcode.SqlErr(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (k DataTemp) UpdateByCond(ctx context.Context, cond *builder.Cond, data interface{}) (err error) {
|
||||
var (
|
||||
query, _ = builder.ToBoundSQL(*cond)
|
||||
model = k.Db.Model(k.modelInstance()).Where(query)
|
||||
)
|
||||
err = model.WithContext(ctx).Updates(data).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func (k DataTemp) UpdateColumnByCond(ctx context.Context, cond *builder.Cond, column string, data interface{}) (err error) {
|
||||
var (
|
||||
query, _ = builder.ToBoundSQL(*cond)
|
||||
model = k.Db.Model(k.modelInstance()).Where(query)
|
||||
)
|
||||
err = model.WithContext(ctx).Update(column, data).Error
|
||||
return err
|
||||
}
|
||||
|
||||
func (k DataTemp) GetByKey(ctx context.Context, key string, value interface{}, data interface{}) (err error) {
|
||||
if err = k.Db.WithContext(ctx).Model(k.modelInstance()).Where(fmt.Sprintf("%s = ?", key), value).Find(data).Error; err != nil {
|
||||
return errcode.SqlErr(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (k DataTemp) DeleteByKey(ctx context.Context, key string, value interface{}) error {
|
||||
result := k.Db.WithContext(ctx).Model(k.modelInstance()).Where(fmt.Sprintf("%s = ?", key), value).
|
||||
Update("deleted_at", gorm.Expr("CURRENT_TIMESTAMP"))
|
||||
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return errcode.NotFound("不存在或已被删除")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package dataTemp
|
||||
|
||||
// ReqPageBo 分页请求实体
|
||||
type ReqPageBo struct {
|
||||
Page int //页码,从第1页开始
|
||||
Limit int //分页大小
|
||||
}
|
||||
|
||||
// GetOffset 获取便宜量
|
||||
// 确保 dataTemp/page.go 中有这些方法
|
||||
func (p *ReqPageBo) GetSize() int {
|
||||
if p == nil {
|
||||
return 10 // 默认每页10条
|
||||
}
|
||||
if p.Limit <= 0 {
|
||||
return 10
|
||||
}
|
||||
return p.Limit
|
||||
}
|
||||
|
||||
func (p *ReqPageBo) GetOffset() int {
|
||||
if p == nil {
|
||||
return 0
|
||||
}
|
||||
return (p.GetPage() - 1) * p.GetSize()
|
||||
}
|
||||
|
||||
func (p *ReqPageBo) GetPage() int {
|
||||
if p == nil || p.Page <= 0 {
|
||||
return 1
|
||||
}
|
||||
return p.Page
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package dataTemp
|
||||
|
||||
// RespPageBo 分页响应实体
|
||||
type RespPageBo struct {
|
||||
Page int //页码
|
||||
Limit int //每页大小
|
||||
Total int64 //总数
|
||||
}
|
||||
|
||||
// SetDataByReq 通过req 设置响应参数
|
||||
func (r *RespPageBo) SetDataByReq(total int64, reqPage *ReqPageBo) *RespPageBo {
|
||||
resp := r
|
||||
if r == nil {
|
||||
resp = &RespPageBo{}
|
||||
}
|
||||
resp.Total = total
|
||||
if reqPage != nil {
|
||||
resp.Page = reqPage.Page
|
||||
resp.Limit = reqPage.Limit
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package errcode
|
||||
|
||||
import "fmt"
|
||||
|
||||
var (
|
||||
AuthNotFound = &BusinessErr{code: AuthErr, message: "账号不存在"}
|
||||
AuthStatusFreeze = &BusinessErr{code: AuthErr, message: "账号冻结"}
|
||||
AuthStatusDel = &BusinessErr{code: AuthErr, message: "身份验证失败"}
|
||||
AuthStatusPwdFail = &BusinessErr{code: AuthErr, message: "密码错误"}
|
||||
AuthTokenCreateFail = &BusinessErr{code: AuthErr, message: "token生成失败"}
|
||||
AuthTokenDelFail = &BusinessErr{code: AuthErr, message: "删除token失败"}
|
||||
AuthWxLoginFail = &BusinessErr{code: AuthErr, message: "微信登录失败,请稍后重试"}
|
||||
AuthInfoFail = &BusinessErr{code: AuthErr, message: "登录异常,请重新登录"}
|
||||
|
||||
TokenNotFound = &BusinessErr{code: TokenErr, message: "缺少 Authorization Header"}
|
||||
TokenFormatErr = &BusinessErr{code: TokenErr, message: "无效的token格式"}
|
||||
TokenInfoNotFound = &BusinessErr{code: TokenErr, message: "未找到用户信息"}
|
||||
TokenInvalid = &BusinessErr{code: TokenErr, message: "token过期"}
|
||||
|
||||
PlatsNotFound = &BusinessErr{code: NotFoundErr, message: "信息未找到"}
|
||||
BadRequest = &BusinessErr{code: BadReqErr, message: "操作失败"}
|
||||
|
||||
ForbiddenError = &BusinessErr{code: ForbiddenErr, message: "权限不足"}
|
||||
Success = &BusinessErr{code: 0, message: "成功"}
|
||||
ParamError = &BusinessErr{code: ParamsErr, message: "参数错误"}
|
||||
|
||||
SystemError = &BusinessErr{code: 405, message: "系统错误"}
|
||||
|
||||
ClientNotFound = &BusinessErr{code: 406, message: "未找到client_id"}
|
||||
SessionNotFound = &BusinessErr{code: 407, message: "未找到会话信息"}
|
||||
UserNotFound = &BusinessErr{code: NotFoundErr, message: "不存在的用户"}
|
||||
|
||||
KeyNotFound = &BusinessErr{code: 409, message: "身份验证失败"}
|
||||
SysNotFound = &BusinessErr{code: 410, message: "未找到系统信息"}
|
||||
SysCodeNotFound = &BusinessErr{code: 411, message: "未找到系统编码"}
|
||||
InvalidParam = &BusinessErr{code: InvalidParamCode, message: "无效参数"}
|
||||
WorkflowError = &BusinessErr{code: 501, message: "工作流过程错误"}
|
||||
ClientInfoNotFound = &BusinessErr{code: NotFoundErr, message: "用户信息未找到"}
|
||||
)
|
||||
|
||||
const (
|
||||
InvalidParamCode = 408
|
||||
AuthErr = 403
|
||||
TokenErr = 401
|
||||
ParamsErr = 422
|
||||
BadReqErr = 400
|
||||
NotFoundErr = 404
|
||||
ForbiddenErr = 403
|
||||
BalanceNotEnoughCode = 402
|
||||
)
|
||||
|
||||
type BusinessErr struct {
|
||||
code int
|
||||
message string
|
||||
}
|
||||
|
||||
func (e *BusinessErr) Error() string {
|
||||
return e.message
|
||||
}
|
||||
func (e *BusinessErr) Code() int {
|
||||
return e.code
|
||||
}
|
||||
|
||||
func NotFound(message string) *BusinessErr {
|
||||
return &BusinessErr{code: NotFoundErr, message: PlatsNotFound.message + ":" + message}
|
||||
}
|
||||
|
||||
func (e *BusinessErr) Is(target error) bool {
|
||||
_, ok := target.(*BusinessErr)
|
||||
return ok
|
||||
}
|
||||
|
||||
// CustomErr 自定义错误
|
||||
func NewBusinessErr(code int, message string) *BusinessErr {
|
||||
return &BusinessErr{code: code, message: message}
|
||||
}
|
||||
|
||||
func SysErrf(message string, arg ...any) *BusinessErr {
|
||||
return &BusinessErr{code: SystemError.code, message: fmt.Sprintf(message, arg)}
|
||||
}
|
||||
|
||||
func SysErr(message string) *BusinessErr {
|
||||
return &BusinessErr{code: SystemError.code, message: message}
|
||||
}
|
||||
|
||||
func ParamErrf(message string, arg ...any) *BusinessErr {
|
||||
return &BusinessErr{code: ParamError.code, message: fmt.Sprintf(message, arg)}
|
||||
}
|
||||
|
||||
func ParamErr(message string) *BusinessErr {
|
||||
return &BusinessErr{code: ParamError.code, message: ParamError.message + ":" + message}
|
||||
}
|
||||
|
||||
func SqlErr(err error) *BusinessErr {
|
||||
|
||||
return &BusinessErr{code: ParamError.code, message: "数据操作失败,请联系管理员处理:" + err.Error()}
|
||||
}
|
||||
|
||||
func BadReq(message string) *BusinessErr {
|
||||
return &BusinessErr{code: BadReqErr, message: BadRequest.message + ":" + message}
|
||||
}
|
||||
|
||||
func Forbidden(message string) *BusinessErr {
|
||||
return &BusinessErr{code: ForbiddenErr, message: ForbiddenError.message + ":" + message}
|
||||
}
|
||||
|
||||
func (e *BusinessErr) Wrap(err error) *BusinessErr {
|
||||
return NewBusinessErr(e.code, err.Error())
|
||||
}
|
||||
|
||||
func BalanceNotEnoughErr(message string) *BusinessErr {
|
||||
return NewBusinessErr(BalanceNotEnoughCode, message)
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"xy_sh/internal/config"
|
||||
"xy_sh/utils/utils_gorm"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Db struct {
|
||||
Client *gorm.DB
|
||||
}
|
||||
|
||||
func NewGormDb(c *config.Config) (*Db, func()) {
|
||||
transDBClient, mf := utils_gorm.DBConn(&c.DB)
|
||||
//directDBClient, df := directDB(c, hLog)
|
||||
cleanup := func() {
|
||||
mf()
|
||||
//df()
|
||||
}
|
||||
return &Db{
|
||||
Client: transDBClient,
|
||||
//DirectDBClient: directDBClient,
|
||||
}, cleanup
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package utils
|
||||
|
||||
import (
|
||||
"github.com/google/wire"
|
||||
)
|
||||
|
||||
var ProviderUtils = wire.NewSet(
|
||||
|
||||
NewGormDb,
|
||||
)
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
package utils_gorm
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
"xy_sh/internal/config"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func DBConn(c *config.DB) (*gorm.DB, func()) {
|
||||
mysqlConn, err := sql.Open(c.Driver, c.Source)
|
||||
gormDB, err := gorm.Open(
|
||||
mysql.New(mysql.Config{Conn: mysqlConn}),
|
||||
)
|
||||
|
||||
gormDB.Logger = NewCustomLogger(gormDB)
|
||||
if err != nil {
|
||||
panic("failed to connect database")
|
||||
}
|
||||
sqlDB, err := gormDB.DB()
|
||||
|
||||
// SetMaxIdleConns sets the maximum number of connections in the idle connection pool.
|
||||
sqlDB.SetMaxIdleConns(int(c.MaxIdle))
|
||||
|
||||
// SetMaxOpenConns sets the maximum number of open connections to the database.
|
||||
sqlDB.SetMaxOpenConns(int(c.MaxLifetime))
|
||||
|
||||
// SetConnMaxLifetime sets the maximum amount of time a connection may be reused.
|
||||
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||
|
||||
return gormDB, func() {
|
||||
if mysqlConn != nil {
|
||||
fmt.Println("关闭 physicalGoodsDB")
|
||||
if err := mysqlConn.Close(); err != nil {
|
||||
fmt.Println("关闭 physicalGoodsDB 失败:", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,96 @@
|
|||
package utils_gorm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CustomLogger struct {
|
||||
gormLogger logger.Interface
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewCustomLogger(db *gorm.DB) *CustomLogger {
|
||||
return &CustomLogger{
|
||||
gormLogger: logger.Default.LogMode(logger.Info),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *CustomLogger) LogMode(level logger.LogLevel) logger.Interface {
|
||||
newlogger := *l
|
||||
newlogger.gormLogger = l.gormLogger.LogMode(level)
|
||||
return &newlogger
|
||||
}
|
||||
|
||||
func (l *CustomLogger) Info(ctx context.Context, msg string, data ...interface{}) {
|
||||
l.gormLogger.Info(ctx, msg, data...)
|
||||
}
|
||||
|
||||
func (l *CustomLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
|
||||
l.gormLogger.Warn(ctx, msg, data...)
|
||||
}
|
||||
|
||||
func (l *CustomLogger) Error(ctx context.Context, msg string, data ...interface{}) {
|
||||
l.gormLogger.Error(ctx, msg, data...)
|
||||
}
|
||||
|
||||
func (l *CustomLogger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) {
|
||||
elapsed := time.Since(begin)
|
||||
sql, _ := fc()
|
||||
l.gormLogger.Trace(ctx, begin, fc, err)
|
||||
operation := extractOperation(sql)
|
||||
tableName := extractTableName(sql)
|
||||
fmt.Println(tableName)
|
||||
//// 将SQL语句保存到数据库
|
||||
if operation == 0 || tableName == "sql_log" {
|
||||
return
|
||||
}
|
||||
//go l.db.Model(&SqlLog{}).Create(&SqlLog{
|
||||
// OperatorID: 1,
|
||||
// OperatorName: "test",
|
||||
// SqlInfo: sql,
|
||||
// TableNames: tableName,
|
||||
// Type: operation,
|
||||
//})
|
||||
|
||||
// 如果有需要,也可以根据执行时间(elapsed)等条件过滤或处理日志记录
|
||||
if elapsed > time.Second {
|
||||
//l.gormLogger.Warn(ctx, "Slow SQL (> 1s): %s", sql)
|
||||
}
|
||||
}
|
||||
|
||||
// extractTableName extracts the table name from a SQL query, supporting quoted table names.
|
||||
func extractTableName(sql string) string {
|
||||
// 使用非捕获组匹配多种SQL操作关键词
|
||||
re := regexp.MustCompile(`(?i)\b(?:from|update|into|delete\s+from)\b\s+[\` + "`" + `"]?(\w+)[\` + "`" + `"]?`)
|
||||
match := re.FindStringSubmatch(sql)
|
||||
|
||||
// 检查是否匹配成功
|
||||
if len(match) > 1 {
|
||||
return match[1]
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// extractOperation extracts the operation type from a SQL query.
|
||||
func extractOperation(sql string) int32 {
|
||||
sql = strings.TrimSpace(strings.ToLower(sql))
|
||||
var operation int32
|
||||
if strings.HasPrefix(sql, "select") {
|
||||
operation = 0
|
||||
} else if strings.HasPrefix(sql, "insert") {
|
||||
operation = 1
|
||||
} else if strings.HasPrefix(sql, "update") {
|
||||
operation = 3
|
||||
} else if strings.HasPrefix(sql, "delete") {
|
||||
operation = 2
|
||||
}
|
||||
return operation
|
||||
}
|
||||
|
|
@ -0,0 +1,311 @@
|
|||
## 文档概述
|
||||
- 接口总数:5个
|
||||
- 环境配置:
|
||||
- 测试环境地址:https://gateway.dev.cdlsxd.cn
|
||||
- 正式环境地址:https://market.api.86698.cn
|
||||
- 测试参数:
|
||||
- app_id: "xxx"
|
||||
- private_key: "xxx"(应用客户私钥,用于请求签名)
|
||||
- public_key: "xxx"(应用平台公钥,用于平台响应或回调验签)
|
||||
- key: "xxxx"(业务参数加密key)
|
||||
- activity_no: "xxxx"(活动编号)
|
||||
- sign_type: "RSA"
|
||||
|
||||
## 认证与安全
|
||||
### 公共Header请求参数
|
||||
| 字段名称 | 类型 | 描述 | 示例值 |
|
||||
|--------|------|------|--------|
|
||||
| Appid | string | 分配给开发者的应用 ID | 123456 |
|
||||
| Timestamp | string | 发送请求的时间,格式 yyyy-MM-dd HH:mm:ss | 2026-06-22 15:30:00 |
|
||||
| Sign | string | 请求签名串 | 详见 SDK 示例 |
|
||||
| Content-Type | string | 请求数据格式 | application/json |
|
||||
|
||||
### 公共请求参数
|
||||
| 字段名称 | 类型 | 描述 | 示例值 |
|
||||
|--------|------|------|--------|
|
||||
| ciphertext | string | 请求业务参数加密串 | 详见 SDK 示例 |
|
||||
|
||||
### 公共响应参数
|
||||
| 字段名称 | 类型 | 描述 |
|
||||
|--------|------|------|
|
||||
| code | int32 | 200 成功 |
|
||||
| message | string | 请求描述 |
|
||||
| reason | string | 错误原因,错误时返回 |
|
||||
| data.ciphertext | string | 业务响应加密串 |
|
||||
|
||||
### 业务参数加密规则
|
||||
1. 将业务参数去掉“零”值的参数,再由小到大按照字母排序,转成json字符串得到plaintext
|
||||
2. 使用应用key将plaintext字符串加密,支持两种模式:aes(ECB模式)/sm4(CBC模式),得到加密业务参数ciphertext
|
||||
|
||||
### 签名规则
|
||||
1. 拼接签名字符串:分配给开发者的应用ID + 发送请求的时间 + 加密业务参数
|
||||
2. 使用应用私钥将拼接待签名字符串生成签名字符串
|
||||
|
||||
### 回调验签规则
|
||||
1. 获取header头里面的签名信息
|
||||
2. 获取body里面的业务参数data
|
||||
3. 将业务参数data去掉“零”值的参数,再由小到大按照字母排序,转成json字符串得到plaintext
|
||||
4. 使用应用key将plaintext字符串加密[aes/sm4]得到加密得到ciphertext
|
||||
5. 拼接签名字符串:分配给开发者的应用ID + 发送请求的时间 + ciphertext
|
||||
6. 使用应用公钥验签
|
||||
|
||||
### 时间戳规则
|
||||
时间格式:yyyy-MM-dd HH:mm:ss,请求时间与服务端时间误差不能超过3分钟
|
||||
|
||||
## 接口列表
|
||||
### 接口 1:获取券码
|
||||
- 路径:/openapi/v1/key/order
|
||||
- 方法:POST
|
||||
- 描述:申请单个券码/权益,支持幂等
|
||||
|
||||
#### 请求参数(业务明文)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 是 | 外部业务号,幂等 |
|
||||
| activity_no | string | 是 | 活动编号 |
|
||||
| account | string | 否 | 账号,按活动类型透传 |
|
||||
| notify_url | string | 否 | 回调通知地址 |
|
||||
|
||||
#### 响应参数(解密后)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 是 | 外部业务号 |
|
||||
| trade_no | string | 是 | 交易号 |
|
||||
| key | string | 否 | 卡密 |
|
||||
| url | string | 否 | 链接型活动返回短链接,key/url 不会同时为空 |
|
||||
| valid_begin_time | string | 否 | 生效时间 |
|
||||
| valid_end_time | string | 否 | 失效时间 |
|
||||
| usable_num | uint32 | 是 | 可用次数 |
|
||||
| usage_num | uint32 | 是 | 已使用次数 |
|
||||
| status | uint32 | 是 | 状态:1 正常,2 已核销,3 已作废 |
|
||||
| settlement_price | float | 否 | 结算价 |
|
||||
| account | string | 否 | 上报账号 |
|
||||
|
||||
#### 示例
|
||||
- 明文业务参数:
|
||||
```json
|
||||
{
|
||||
"out_biz_no": "order_001",
|
||||
"activity_no": "ACT20260622001",
|
||||
"account": "18666666666",
|
||||
"notify_url": "https://notify.example.com/openapi"
|
||||
}
|
||||
```
|
||||
- 请求体示例:
|
||||
```json
|
||||
{
|
||||
"ciphertext": "加密后的业务报文"
|
||||
}
|
||||
```
|
||||
- 异常响应示例:
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "Signature verification failed",
|
||||
"reason": "INVALID_SIGNATURE"
|
||||
}
|
||||
```
|
||||
- 成功响应示例:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"ciphertext": "加密后的响应报文"
|
||||
},
|
||||
"message": "成功"
|
||||
}
|
||||
```
|
||||
- 解密后成功示例:
|
||||
```json
|
||||
{
|
||||
"out_biz_no": "order_001",
|
||||
"trade_no": "7251449503000383488",
|
||||
"key": "aZKdU9BymzR6qGRzJM",
|
||||
"url": "",
|
||||
"valid_begin_time": "2026-06-22 15:30:00",
|
||||
"valid_end_time": "2026-12-31 23:59:59",
|
||||
"usable_num": 1,
|
||||
"usage_num": 0,
|
||||
"status": 1,
|
||||
"settlement_price": 9.9,
|
||||
"account": "18666666666"
|
||||
}
|
||||
```
|
||||
|
||||
### 接口 2:券码查询
|
||||
- 路径:/openapi/v1/key/query
|
||||
- 方法:POST
|
||||
- 描述:查询已申请的券码详情
|
||||
|
||||
#### 请求参数(业务明文)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 否 | 外部业务号,与 trade_no 二选一 |
|
||||
| trade_no | string | 否 | 交易号,与 out_biz_no 二选一 |
|
||||
|
||||
#### 响应参数(解密后)
|
||||
与“获取券码”响应参数完全一致。
|
||||
|
||||
#### 示例
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"out_biz_no": "order_001",
|
||||
"trade_no": "7251449503000383488",
|
||||
"key": "aZKdU9BymzR6qGRzJM",
|
||||
"url": "",
|
||||
"valid_begin_time": "2026-06-22 15:30:00",
|
||||
"valid_end_time": "2026-12-31 23:59:59",
|
||||
"usable_num": 1,
|
||||
"usage_num": 0,
|
||||
"status": 1,
|
||||
"settlement_price": 9.9,
|
||||
"account": "18666666666",
|
||||
"ciphertext": ""
|
||||
},
|
||||
"message": "成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 接口 3:券码作废
|
||||
- 路径:/openapi/v1/key/discard
|
||||
- 方法:POST
|
||||
- 描述:作废已申请的券码
|
||||
|
||||
#### 请求参数(业务明文)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 否 | 外部业务号,与 trade_no 二选一 |
|
||||
| trade_no | string | 否 | 交易号,与 out_biz_no 二选一 |
|
||||
|
||||
#### 响应参数(解密后)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 是 | 外部业务号 |
|
||||
| trade_no | string | 是 | 交易号 |
|
||||
| status | uint32 | 是 | 3 表示已作废 |
|
||||
|
||||
#### 示例
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"out_biz_no": "order_001",
|
||||
"trade_no": "7251449503000383488",
|
||||
"status": 3,
|
||||
"ciphertext": ""
|
||||
},
|
||||
"message": "成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 接口 4:批量发卡
|
||||
- 路径:/openapi/v1/key/batch_order
|
||||
- 方法:POST
|
||||
- 描述:批量申请多个券码,返回异步任务状态
|
||||
|
||||
#### 请求参数(业务明文)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 是 | 外部业务号,幂等 |
|
||||
| activity_no | string | 是 | 活动编号 |
|
||||
| number | int32 | 是 | 发卡数量 |
|
||||
| notify_url | string | 否 | 回调通知地址 |
|
||||
|
||||
#### 响应参数(解密后)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 是 | 外部业务号 |
|
||||
| trade_no | string | 是 | 交易号 |
|
||||
| status | string | 是 | 任务状态,初始返回 processing |
|
||||
|
||||
#### 示例
|
||||
- 成功响应示例:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"ciphertext": "加密后的响应报文"
|
||||
},
|
||||
"message": "成功"
|
||||
}
|
||||
```
|
||||
- 解密后示例:
|
||||
```json
|
||||
{
|
||||
"out_biz_no": "batch_001",
|
||||
"trade_no": "7251449503000383499",
|
||||
"status": "processing"
|
||||
}
|
||||
```
|
||||
|
||||
### 接口 5:批量查询
|
||||
- 路径:/openapi/v1/key/batch_query
|
||||
- 方法:POST
|
||||
- 描述:查询批量发卡任务的状态和结果
|
||||
|
||||
#### 请求参数(业务明文)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 否 | 外部业务号,与 trade_no 二选一 |
|
||||
| trade_no | string | 否 | 交易号,与 out_biz_no 二选一 |
|
||||
|
||||
#### 响应参数(解密后)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 是 | 外部业务号 |
|
||||
| trade_no | string | 是 | 交易号 |
|
||||
| status | string | 是 | processing / success / failed |
|
||||
| download_url | string | 否 | 批量任务成功后返回下载地址 |
|
||||
| zip_password | string | 否 | 批量任务成功后返回压缩包密码 |
|
||||
|
||||
#### 示例
|
||||
- 成功响应示例:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"ciphertext": "加密后的响应报文"
|
||||
},
|
||||
"message": "成功"
|
||||
}
|
||||
```
|
||||
- 解密后示例:
|
||||
```json
|
||||
{
|
||||
"out_biz_no": "batch_001",
|
||||
"trade_no": "7251449503000383499",
|
||||
"status": "success",
|
||||
"download_url": "https://oss.example.com/openapi\_7251449503000383499.zip",
|
||||
"zip_password": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
## 错误码
|
||||
| 错误码 | 说明 | 处理建议 |
|
||||
|--------|------|----------|
|
||||
| 500 | PANIC/其它 系统错误 | 联系平台处理 |
|
||||
| 400 | INVALID_PAYLOAD 请求外壳格式错误 | 请检查请求 JSON 结构 |
|
||||
| 400 | MISSING_PARAM 缺少必要参数 | 请检查 app_id、timestamp、sign、ciphertext |
|
||||
| 400 | INVALID_TIMESTAMP 时间格式错误 | 请检查时间格式 |
|
||||
| 400 | DECRYPT_FAILED 业务参数解密失败 | 请检查加密方式与密钥 |
|
||||
| 400 | PARAM_FAIL 参数错误 | 请检查业务参数 |
|
||||
| 400 | PARAM_DECRYPT_FAIL 明文参数格式错误 | 请检查密文解密后的业务报文 |
|
||||
| 401 | APP_NOT_FOUND 应用不存在 | 请检查应用 ID |
|
||||
| 401 | INVALID_SIGNATURE 签名错误 | 请检查签名串与私钥 |
|
||||
| 401 | EXPIRED_TIMESTAMP 请求已过期 | 请检查客户端时间 |
|
||||
| 401 | ACTIVITY_NOT_AUTH 活动未授权 | 请检查活动授权状态 |
|
||||
| 401 | MERCHANT_NOT_EXIST 客户不存在 | 请检查客户是否存在 |
|
||||
| 401 | MERCHANT_NOT_AUTH 客户冻结 | 请检查客户授权状态 |
|
||||
| 401 | MERCHANT_APP_INCOMPLETE 客户应用配置未完善 | 请检查客户应用配置 |
|
||||
| 401 | MERCHANT_APP_NOT_AUTH 应用不存在或未授权 | 请检查客户应用授权状态 |
|
||||
| 403 | ACTIVITY_EXPIRE 活动已结束 | 请检查活动是否有效 |
|
||||
| 403 | ACTIVITY_OUT_OF_STOCK 活动剩余量不足 | 请检查活动库存 |
|
||||
| 404 | ACTIVITY_NOT_EXIST 活动不存在 | 请检查活动编号 |
|
||||
| 404 | MERCHANT_ORDER_NOT_EXIST 订单不存在 | 请检查交易号或外部业务号 |
|
||||
| 404 | KEY_NOT_EXIST key码不存在 | 请检查订单信息 |
|
||||
| 429 | DUPLICATE_REQUEST 重复请求,请稍后重试 | 请避免短时间内重复提交同一业务号 |
|
||||
|
||||
## 回调通知
|
||||
平台会将异步任务结果推送到请求中指定的notify_url地址,回调验签规则参考上述认证与安全章节的回调验签流程。
|
||||
|
|
@ -0,0 +1,240 @@
|
|||
package ymt_v3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Env 环境
|
||||
type Env string
|
||||
|
||||
const (
|
||||
EnvTest Env = "test"
|
||||
EnvProduct Env = "product"
|
||||
)
|
||||
|
||||
// 环境地址
|
||||
const (
|
||||
testBaseURL = "https://gateway.dev.cdlsxd.cn"
|
||||
productBaseURL = "https://market.api.86698.cn"
|
||||
)
|
||||
|
||||
// ClientConfig 客户端配置
|
||||
type ClientConfig struct {
|
||||
AppID string // 应用ID
|
||||
PrivateKey string // 应用私钥(PEM格式)
|
||||
PublicKey string // 平台公钥(PEM格式)
|
||||
Key string // 业务参数加密key
|
||||
EncryptType EncryptType // 加密类型:aes 或 sm4
|
||||
Env Env // 环境:test 或 product
|
||||
HTTPClient *http.Client // HTTP客户端,可选
|
||||
}
|
||||
|
||||
// Client SDK客户端
|
||||
type Client struct {
|
||||
config *ClientConfig
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
}
|
||||
|
||||
// NewClient 创建新的SDK客户端
|
||||
func NewClient(config *ClientConfig) (*Client, error) {
|
||||
if config.AppID == "" {
|
||||
return nil, fmt.Errorf("AppID不能为空")
|
||||
}
|
||||
if config.PrivateKey == "" {
|
||||
return nil, fmt.Errorf("PrivateKey不能为空")
|
||||
}
|
||||
if config.PublicKey == "" {
|
||||
return nil, fmt.Errorf("PublicKey不能为空")
|
||||
}
|
||||
if config.Key == "" {
|
||||
return nil, fmt.Errorf("Key不能为空")
|
||||
}
|
||||
if config.EncryptType == "" {
|
||||
config.EncryptType = EncryptTypeAES
|
||||
}
|
||||
if config.Env == "" {
|
||||
config.Env = EnvTest
|
||||
}
|
||||
|
||||
var baseURL string
|
||||
switch config.Env {
|
||||
case EnvTest:
|
||||
baseURL = testBaseURL
|
||||
case EnvProduct:
|
||||
baseURL = productBaseURL
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的环境: %s", config.Env)
|
||||
}
|
||||
|
||||
httpClient := config.HTTPClient
|
||||
if httpClient == nil {
|
||||
httpClient = &http.Client{}
|
||||
}
|
||||
|
||||
return &Client{
|
||||
config: config,
|
||||
httpClient: httpClient,
|
||||
baseURL: baseURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// doRequest 发送请求并处理响应
|
||||
func (c *Client) doRequest(path string, bizParams interface{}, result interface{}) error {
|
||||
// 1. 加密业务参数
|
||||
ciphertext, err := EncryptBizParams(bizParams, []byte(c.config.Key), c.config.EncryptType)
|
||||
if err != nil {
|
||||
return NewSDKError("加密业务参数失败", err)
|
||||
}
|
||||
|
||||
// 2. 生成时间戳
|
||||
timestamp := GenerateTimestamp()
|
||||
|
||||
// 3. 构建签名字符串并签名
|
||||
signStr := BuildSignStr(c.config.AppID, timestamp, ciphertext)
|
||||
sign, err := SignWithRSA(signStr, c.config.PrivateKey)
|
||||
if err != nil {
|
||||
return NewSDKError("签名失败", err)
|
||||
}
|
||||
|
||||
// 4. 构建请求体
|
||||
reqBody := map[string]string{
|
||||
"ciphertext": ciphertext,
|
||||
}
|
||||
reqBodyBytes, err := json.Marshal(reqBody)
|
||||
if err != nil {
|
||||
return NewSDKError("序列化请求体失败", err)
|
||||
}
|
||||
|
||||
// 5. 创建HTTP请求
|
||||
url := c.baseURL + path
|
||||
req, err := http.NewRequest("POST", url, bytes.NewReader(reqBodyBytes))
|
||||
if err != nil {
|
||||
return NewSDKError("创建HTTP请求失败", err)
|
||||
}
|
||||
|
||||
// 6. 设置Header
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Appid", c.config.AppID)
|
||||
req.Header.Set("Timestamp", timestamp)
|
||||
req.Header.Set("Sign", sign)
|
||||
|
||||
// 7. 发送请求
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return NewSDKError("发送HTTP请求失败", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 8. 读取响应
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return NewSDKError("读取响应失败", err)
|
||||
}
|
||||
|
||||
// 9. 解析公共响应
|
||||
var commonResp CommonResponse
|
||||
if err := json.Unmarshal(respBody, &commonResp); err != nil {
|
||||
return NewSDKError("解析响应JSON失败", err)
|
||||
}
|
||||
|
||||
// 10. 检查业务错误
|
||||
if commonResp.Code != 200 {
|
||||
return NewAPIError(commonResp.Code, commonResp.Message, commonResp.Reason)
|
||||
}
|
||||
|
||||
// 11. 检查是否有加密数据
|
||||
if commonResp.Data == nil || commonResp.Data.Ciphertext == "" {
|
||||
return NewSDKError("响应中没有加密数据", nil)
|
||||
}
|
||||
|
||||
// 12. 解密响应数据
|
||||
decrypted, err := DecryptBizParams(commonResp.Data.Ciphertext, []byte(c.config.Key), c.config.EncryptType)
|
||||
if err != nil {
|
||||
return NewSDKError("解密响应数据失败", err)
|
||||
}
|
||||
|
||||
// 13. 解析解密后的数据到结果
|
||||
if err := json.Unmarshal(decrypted, result); err != nil {
|
||||
return NewSDKError("解析解密后的响应数据失败", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ==================== API方法 ====================
|
||||
|
||||
// KeyOrder 获取券码
|
||||
// POST /openapi/v1/key/order
|
||||
func (c *Client) KeyOrder(req *KeyOrderRequest) (*KeyOrderResponse, error) {
|
||||
result := &KeyOrderResponse{}
|
||||
if err := c.doRequest("/openapi/v1/key/order", req, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// KeyQuery 券码查询
|
||||
// POST /openapi/v1/key/query
|
||||
func (c *Client) KeyQuery(req *KeyQueryRequest) (*KeyOrderResponse, error) {
|
||||
result := &KeyOrderResponse{}
|
||||
if err := c.doRequest("/openapi/v1/key/query", req, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// KeyDiscard 券码作废
|
||||
// POST /openapi/v1/key/discard
|
||||
func (c *Client) KeyDiscard(req *KeyDiscardRequest) (*KeyDiscardResponse, error) {
|
||||
result := &KeyDiscardResponse{}
|
||||
if err := c.doRequest("/openapi/v1/key/discard", req, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// BatchOrder 批量发卡
|
||||
// POST /openapi/v1/key/batch_order
|
||||
func (c *Client) BatchOrder(req *BatchOrderRequest) (*BatchOrderResponse, error) {
|
||||
result := &BatchOrderResponse{}
|
||||
if err := c.doRequest("/openapi/v1/key/batch_order", req, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// BatchQuery 批量查询
|
||||
// POST /openapi/v1/key/batch_query
|
||||
func (c *Client) BatchQuery(req *BatchQueryRequest) (*BatchQueryResponse, error) {
|
||||
result := &BatchQueryResponse{}
|
||||
if err := c.doRequest("/openapi/v1/key/batch_query", req, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// VerifyNotify 验证回调通知
|
||||
func (c *Client) VerifyNotify(header *NotifyHeader, data interface{}) error {
|
||||
return VerifyNotifySign(header, data, []byte(c.config.Key), c.config.PublicKey, c.config.EncryptType)
|
||||
}
|
||||
|
||||
// GetBaseURL 获取当前环境的基础URL
|
||||
func (c *Client) GetBaseURL() string {
|
||||
return c.baseURL
|
||||
}
|
||||
|
||||
// GetAppID 获取应用ID
|
||||
func (c *Client) GetAppID() string {
|
||||
return c.config.AppID
|
||||
}
|
||||
|
||||
// String 返回客户端配置的字符串表示(隐藏敏感信息)
|
||||
func (c *Client) String() string {
|
||||
return fmt.Sprintf("Client{AppID: %s, Env: %s, BaseURL: %s, EncryptType: %s}",
|
||||
c.config.AppID, c.config.Env, c.baseURL, c.config.EncryptType)
|
||||
}
|
||||
|
|
@ -0,0 +1,409 @@
|
|||
package ymt_v3
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tjfoc/gmsm/sm4"
|
||||
)
|
||||
|
||||
// EncryptType 加密类型
|
||||
type EncryptType string
|
||||
|
||||
const (
|
||||
// EncryptTypeAES AES-ECB加密
|
||||
EncryptTypeAES EncryptType = "aes"
|
||||
// EncryptTypeSM4 SM4-CBC加密
|
||||
EncryptTypeSM4 EncryptType = "sm4"
|
||||
)
|
||||
|
||||
// ==================== RSA签名 ====================
|
||||
|
||||
// SignWithRSA 使用RSA私钥对数据进行签名
|
||||
func SignWithRSA(signStr string, privateKeyPEM string) (string, error) {
|
||||
block, _ := pem.Decode([]byte(privateKeyPEM))
|
||||
if block == nil {
|
||||
return "", fmt.Errorf("failed to decode PEM private key")
|
||||
}
|
||||
|
||||
privateKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse private key: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
rsaPrivateKey, ok := privateKey.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("not a RSA private key")
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
h.Write([]byte(signStr))
|
||||
hashed := h.Sum(nil)
|
||||
|
||||
signature, err := rsa.SignPKCS1v15(rand.Reader, rsaPrivateKey, crypto.SHA256, hashed)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(signature), nil
|
||||
}
|
||||
|
||||
// VerifyWithRSA 使用RSA公钥验证签名
|
||||
func VerifyWithRSA(signStr, signature string, publicKeyPEM string) error {
|
||||
block, _ := pem.Decode([]byte(publicKeyPEM))
|
||||
if block == nil {
|
||||
return fmt.Errorf("failed to decode PEM public key")
|
||||
}
|
||||
|
||||
publicKey, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse public key: %v", err)
|
||||
}
|
||||
|
||||
rsaPublicKey, ok := publicKey.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return fmt.Errorf("not a RSA public key")
|
||||
}
|
||||
|
||||
h := sha256.New()
|
||||
h.Write([]byte(signStr))
|
||||
hashed := h.Sum(nil)
|
||||
|
||||
sigBytes, err := base64.StdEncoding.DecodeString(signature)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to decode signature: %v", err)
|
||||
}
|
||||
|
||||
return rsa.VerifyPKCS1v15(rsaPublicKey, crypto.SHA256, hashed, sigBytes)
|
||||
}
|
||||
|
||||
// ==================== AES ECB加密 ====================
|
||||
|
||||
// AESECBEncrypt AES-ECB模式加密
|
||||
func AESECBEncrypt(plaintext []byte, key []byte) (string, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建AES cipher失败: %v", err)
|
||||
}
|
||||
|
||||
padded := pkcs7Padding(plaintext, aes.BlockSize)
|
||||
|
||||
ciphertext := make([]byte, len(padded))
|
||||
for i := 0; i < len(padded); i += aes.BlockSize {
|
||||
block.Encrypt(ciphertext[i:i+aes.BlockSize], padded[i:i+aes.BlockSize])
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
||||
}
|
||||
|
||||
// AESECBDecrypt AES-ECB模式解密
|
||||
func AESECBDecrypt(encryptedData string, key []byte) ([]byte, error) {
|
||||
ciphertext, err := base64.StdEncoding.DecodeString(encryptedData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解码失败: %v", err)
|
||||
}
|
||||
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建AES cipher失败: %v", err)
|
||||
}
|
||||
|
||||
if len(ciphertext)%aes.BlockSize != 0 {
|
||||
return nil, fmt.Errorf("密文长度不是块大小的整数倍")
|
||||
}
|
||||
|
||||
plaintext := make([]byte, len(ciphertext))
|
||||
for i := 0; i < len(ciphertext); i += aes.BlockSize {
|
||||
block.Decrypt(plaintext[i:i+aes.BlockSize], ciphertext[i:i+aes.BlockSize])
|
||||
}
|
||||
|
||||
plaintext, err = pkcs7UnPadding(plaintext)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("去除填充失败: %v", err)
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// ==================== SM4 CBC加密 ====================
|
||||
|
||||
// SM4CBCEncrypt SM4-CBC模式加密
|
||||
// IV会前置到密文中,解密时自动提取
|
||||
func SM4CBCEncrypt(plaintext []byte, key []byte) (string, error) {
|
||||
if len(key) != 16 {
|
||||
return "", fmt.Errorf("SM4密钥长度必须为16字节")
|
||||
}
|
||||
|
||||
block, err := sm4.NewCipher(key)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建SM4 cipher失败: %v", err)
|
||||
}
|
||||
|
||||
padded := pkcs7Padding(plaintext, block.BlockSize())
|
||||
|
||||
iv := make([]byte, block.BlockSize())
|
||||
if _, err := io.ReadFull(rand.Reader, iv); err != nil {
|
||||
return "", fmt.Errorf("生成IV失败: %v", err)
|
||||
}
|
||||
|
||||
mode := cipher.NewCBCEncrypter(block, iv)
|
||||
ciphertext := make([]byte, len(padded))
|
||||
mode.CryptBlocks(ciphertext, padded)
|
||||
|
||||
// IV前置到密文
|
||||
result := append(iv, ciphertext...)
|
||||
return base64.StdEncoding.EncodeToString(result), nil
|
||||
}
|
||||
|
||||
// SM4CBCDecrypt SM4-CBC模式解密
|
||||
// 从密文中提取前置的IV进行解密
|
||||
func SM4CBCDecrypt(encryptedData string, key []byte) ([]byte, error) {
|
||||
if len(key) != 16 {
|
||||
return nil, fmt.Errorf("SM4密钥长度必须为16字节")
|
||||
}
|
||||
|
||||
data, err := base64.StdEncoding.DecodeString(encryptedData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解码失败: %v", err)
|
||||
}
|
||||
|
||||
block, err := sm4.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建SM4 cipher失败: %v", err)
|
||||
}
|
||||
|
||||
blockSize := block.BlockSize()
|
||||
if len(data) < blockSize*2 {
|
||||
return nil, fmt.Errorf("数据长度不足")
|
||||
}
|
||||
iv := data[:blockSize]
|
||||
ciphertext := data[blockSize:]
|
||||
|
||||
mode := cipher.NewCBCDecrypter(block, iv)
|
||||
plaintext := make([]byte, len(ciphertext))
|
||||
mode.CryptBlocks(plaintext, ciphertext)
|
||||
|
||||
plaintext, err = pkcs7UnPadding(plaintext)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("去除填充失败: %v", err)
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// ==================== PKCS7填充 ====================
|
||||
|
||||
// pkcs7Padding PKCS7填充
|
||||
func pkcs7Padding(data []byte, blockSize int) []byte {
|
||||
padding := blockSize - len(data)%blockSize
|
||||
padText := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||
return append(data, padText...)
|
||||
}
|
||||
|
||||
// pkcs7UnPadding 去除PKCS7填充
|
||||
func pkcs7UnPadding(data []byte) ([]byte, error) {
|
||||
length := len(data)
|
||||
if length == 0 {
|
||||
return nil, fmt.Errorf("数据为空")
|
||||
}
|
||||
padding := int(data[length-1])
|
||||
if padding > length || padding == 0 {
|
||||
return nil, fmt.Errorf("无效的填充")
|
||||
}
|
||||
for i := length - padding; i < length; i++ {
|
||||
if data[i] != byte(padding) {
|
||||
return nil, fmt.Errorf("无效的填充")
|
||||
}
|
||||
}
|
||||
return data[:length-padding], nil
|
||||
}
|
||||
|
||||
// ==================== 业务参数加密/解密 ====================
|
||||
|
||||
// EncryptBizParams 加密业务参数
|
||||
// 1. 将业务参数去掉"零"值的参数,再由小到大按照字母排序,转成json字符串得到plaintext
|
||||
// 2. 使用应用key将plaintext字符串加密,得到ciphertext
|
||||
func EncryptBizParams(params interface{}, key []byte, encType EncryptType) (string, error) {
|
||||
plaintext, err := marshalWithoutZero(params)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("序列化业务参数失败: %v", err)
|
||||
}
|
||||
|
||||
switch encType {
|
||||
case EncryptTypeAES:
|
||||
return AESECBEncrypt([]byte(plaintext), key)
|
||||
case EncryptTypeSM4:
|
||||
return SM4CBCEncrypt([]byte(plaintext), key)
|
||||
default:
|
||||
return "", fmt.Errorf("不支持的加密类型: %s", encType)
|
||||
}
|
||||
}
|
||||
|
||||
// DecryptBizParams 解密业务参数
|
||||
func DecryptBizParams(ciphertext string, key []byte, encType EncryptType) ([]byte, error) {
|
||||
switch encType {
|
||||
case EncryptTypeAES:
|
||||
return AESECBDecrypt(ciphertext, key)
|
||||
case EncryptTypeSM4:
|
||||
return SM4CBCDecrypt(ciphertext, key)
|
||||
default:
|
||||
return nil, fmt.Errorf("不支持的加密类型: %s", encType)
|
||||
}
|
||||
}
|
||||
|
||||
// EncryptBizParamsRaw 直接加密字节数据(用于回调验签)
|
||||
func EncryptBizParamsRaw(plaintext []byte, key []byte, encType EncryptType) (string, error) {
|
||||
switch encType {
|
||||
case EncryptTypeAES:
|
||||
return AESECBEncrypt(plaintext, key)
|
||||
case EncryptTypeSM4:
|
||||
return SM4CBCEncrypt(plaintext, key)
|
||||
default:
|
||||
return "", fmt.Errorf("不支持的加密类型: %s", encType)
|
||||
}
|
||||
}
|
||||
|
||||
// marshalWithoutZero 将结构体序列化为JSON,去掉零值字段并按字母排序
|
||||
func marshalWithoutZero(v interface{}) (string, error) {
|
||||
val := reflect.ValueOf(v)
|
||||
if val.Kind() == reflect.Ptr {
|
||||
val = val.Elem()
|
||||
}
|
||||
|
||||
if val.Kind() != reflect.Struct {
|
||||
if val.Kind() == reflect.Map {
|
||||
// 类型断言为 map[string]interface{}
|
||||
m, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
// 尝试从 reflect 转换
|
||||
m = make(map[string]interface{})
|
||||
for _, key := range val.MapKeys() {
|
||||
m[fmt.Sprintf("%v", key.Interface())] = val.MapIndex(key).Interface()
|
||||
}
|
||||
}
|
||||
return marshalMapSorted(m)
|
||||
}
|
||||
data, err := json.Marshal(v)
|
||||
return string(data), err
|
||||
}
|
||||
|
||||
result := make(map[string]interface{})
|
||||
typ := val.Type()
|
||||
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := val.Field(i)
|
||||
fieldType := typ.Field(i)
|
||||
|
||||
jsonTag := fieldType.Tag.Get("json")
|
||||
if jsonTag == "" || jsonTag == "-" {
|
||||
continue
|
||||
}
|
||||
name := strings.Split(jsonTag, ",")[0]
|
||||
|
||||
if isZeroValue(field) {
|
||||
continue
|
||||
}
|
||||
|
||||
result[name] = field.Interface()
|
||||
}
|
||||
|
||||
return marshalMapSorted(result)
|
||||
}
|
||||
|
||||
// marshalMapSorted 将map按键排序后序列化为JSON
|
||||
func marshalMapSorted(m map[string]interface{}) (string, error) {
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
var buf bytes.Buffer
|
||||
buf.WriteByte('{')
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
buf.WriteByte(',')
|
||||
}
|
||||
keyBytes, _ := json.Marshal(k)
|
||||
buf.Write(keyBytes)
|
||||
buf.WriteByte(':')
|
||||
valBytes, err := json.Marshal(m[k])
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
buf.Write(valBytes)
|
||||
}
|
||||
buf.WriteByte('}')
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// isZeroValue 判断reflect.Value是否为零值
|
||||
func isZeroValue(v reflect.Value) bool {
|
||||
switch v.Kind() {
|
||||
case reflect.String:
|
||||
return v.String() == ""
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return v.Int() == 0
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return v.Uint() == 0
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return v.Float() == 0
|
||||
case reflect.Bool:
|
||||
return !v.Bool()
|
||||
case reflect.Slice, reflect.Map, reflect.Ptr:
|
||||
return v.IsNil()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 签名相关 ====================
|
||||
|
||||
// BuildSignStr 构建签名字符串
|
||||
// 规则:app_id + timestamp + ciphertext
|
||||
func BuildSignStr(appID, timestamp, ciphertext string) string {
|
||||
return appID + timestamp + ciphertext
|
||||
}
|
||||
|
||||
// GenerateTimestamp 生成时间戳,格式 yyyy-MM-dd HH:mm:ss
|
||||
func GenerateTimestamp() string {
|
||||
return time.Now().Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
// ==================== 回调验签 ====================
|
||||
|
||||
// VerifyNotifySign 验证回调通知签名
|
||||
func VerifyNotifySign(header *NotifyHeader, data interface{}, key []byte, publicKeyPEM string, encType EncryptType) error {
|
||||
plaintext, err := marshalWithoutZero(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化回调数据失败: %v", err)
|
||||
}
|
||||
|
||||
ciphertext, err := EncryptBizParamsRaw([]byte(plaintext), key, encType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("加密回调数据失败: %v", err)
|
||||
}
|
||||
|
||||
signStr := BuildSignStr(header.Appid, header.Timestamp, ciphertext)
|
||||
|
||||
return VerifyWithRSA(signStr, header.Sign, publicKeyPEM)
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
package ymt_v3
|
||||
|
||||
import "fmt"
|
||||
|
||||
// APIError 表示API返回的业务错误
|
||||
type APIError struct {
|
||||
Code int32 `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
return fmt.Sprintf("API error: code=%d, message=%s, reason=%s", e.Code, e.Message, e.Reason)
|
||||
}
|
||||
|
||||
// IsSuccess 判断是否成功
|
||||
func (e *APIError) IsSuccess() bool {
|
||||
return e.Code == 200
|
||||
}
|
||||
|
||||
// NewAPIError 创建API错误
|
||||
func NewAPIError(code int32, message, reason string) *APIError {
|
||||
return &APIError{
|
||||
Code: code,
|
||||
Message: message,
|
||||
Reason: reason,
|
||||
}
|
||||
}
|
||||
|
||||
// SDKError SDK内部错误
|
||||
type SDKError struct {
|
||||
Message string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *SDKError) Error() string {
|
||||
if e.Err != nil {
|
||||
return fmt.Sprintf("SDK error: %s: %v", e.Message, e.Err)
|
||||
}
|
||||
return fmt.Sprintf("SDK error: %s", e.Message)
|
||||
}
|
||||
|
||||
func (e *SDKError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// NewSDKError 创建SDK内部错误
|
||||
func NewSDKError(msg string, err error) *SDKError {
|
||||
return &SDKError{Message: msg, Err: err}
|
||||
}
|
||||
|
|
@ -0,0 +1,324 @@
|
|||
package ymt_v3
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 测试参数(请替换为实际值)
|
||||
const (
|
||||
testAppID = "your_app_id"
|
||||
testPrivateKey = `-----BEGIN PRIVATE KEY-----
|
||||
your_private_key_here
|
||||
-----END PRIVATE KEY-----`
|
||||
testPublicKey = `-----BEGIN PUBLIC KEY-----
|
||||
your_public_key_here
|
||||
-----END PUBLIC KEY-----`
|
||||
testKey = "your_encryption_key_16"
|
||||
testActivityNo = "your_activity_no"
|
||||
)
|
||||
|
||||
func ExampleClient_KeyOrder() {
|
||||
// 创建客户端
|
||||
client, err := NewClient(&ClientConfig{
|
||||
AppID: testAppID,
|
||||
PrivateKey: testPrivateKey,
|
||||
PublicKey: testPublicKey,
|
||||
Key: testKey,
|
||||
EncryptType: EncryptTypeAES,
|
||||
Env: EnvTest,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("创建客户端失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 获取券码
|
||||
resp, err := client.KeyOrder(&KeyOrderRequest{
|
||||
OutBizNo: "order_001",
|
||||
ActivityNo: testActivityNo,
|
||||
Account: "18666666666",
|
||||
NotifyURL: "https://notify.example.com/openapi",
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("获取券码失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("交易号: %s\n", resp.TradeNo)
|
||||
fmt.Printf("卡密: %s\n", resp.Key)
|
||||
fmt.Printf("状态: %d\n", resp.Status)
|
||||
|
||||
// Output:
|
||||
// 交易号: 7251449503000383488
|
||||
// 卡密: aZKdU9BymzR6qGRzJM
|
||||
// 状态: 1
|
||||
}
|
||||
|
||||
func ExampleClient_KeyQuery() {
|
||||
client, _ := NewClient(&ClientConfig{
|
||||
AppID: testAppID,
|
||||
PrivateKey: testPrivateKey,
|
||||
PublicKey: testPublicKey,
|
||||
Key: testKey,
|
||||
EncryptType: EncryptTypeAES,
|
||||
Env: EnvTest,
|
||||
})
|
||||
|
||||
// 按外部业务号查询
|
||||
resp, err := client.KeyQuery(&KeyQueryRequest{
|
||||
OutBizNo: "order_001",
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("查询失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("交易号: %s\n", resp.TradeNo)
|
||||
fmt.Printf("状态: %d\n", resp.Status)
|
||||
}
|
||||
|
||||
func ExampleClient_KeyDiscard() {
|
||||
client, _ := NewClient(&ClientConfig{
|
||||
AppID: testAppID,
|
||||
PrivateKey: testPrivateKey,
|
||||
PublicKey: testPublicKey,
|
||||
Key: testKey,
|
||||
EncryptType: EncryptTypeAES,
|
||||
Env: EnvTest,
|
||||
})
|
||||
|
||||
// 按交易号作废
|
||||
resp, err := client.KeyDiscard(&KeyDiscardRequest{
|
||||
TradeNo: "7251449503000383488",
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("作废失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("交易号: %s\n", resp.TradeNo)
|
||||
fmt.Printf("状态: %d\n", resp.Status)
|
||||
}
|
||||
|
||||
func ExampleClient_BatchOrder() {
|
||||
client, _ := NewClient(&ClientConfig{
|
||||
AppID: testAppID,
|
||||
PrivateKey: testPrivateKey,
|
||||
PublicKey: testPublicKey,
|
||||
Key: testKey,
|
||||
EncryptType: EncryptTypeAES,
|
||||
Env: EnvTest,
|
||||
})
|
||||
|
||||
// 批量发卡
|
||||
resp, err := client.BatchOrder(&BatchOrderRequest{
|
||||
OutBizNo: "batch_001",
|
||||
ActivityNo: testActivityNo,
|
||||
Number: 100,
|
||||
NotifyURL: "https://notify.example.com/openapi",
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("批量发卡失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("交易号: %s\n", resp.TradeNo)
|
||||
fmt.Printf("状态: %s\n", resp.Status)
|
||||
}
|
||||
|
||||
func ExampleClient_BatchQuery() {
|
||||
client, _ := NewClient(&ClientConfig{
|
||||
AppID: testAppID,
|
||||
PrivateKey: testPrivateKey,
|
||||
PublicKey: testPublicKey,
|
||||
Key: testKey,
|
||||
EncryptType: EncryptTypeAES,
|
||||
Env: EnvTest,
|
||||
})
|
||||
|
||||
// 查询批量任务
|
||||
resp, err := client.BatchQuery(&BatchQueryRequest{
|
||||
TradeNo: "7251449503000383499",
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("查询失败: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("状态: %s\n", resp.Status)
|
||||
if resp.DownloadURL != "" {
|
||||
fmt.Printf("下载地址: %s\n", resp.DownloadURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalWithoutZero(t *testing.T) {
|
||||
req := KeyOrderRequest{
|
||||
OutBizNo: "order_001",
|
||||
ActivityNo: "ACT20260622001",
|
||||
// Account和NotifyURL为空,应该被去掉
|
||||
}
|
||||
|
||||
result, err := marshalWithoutZero(req)
|
||||
if err != nil {
|
||||
t.Fatalf("marshalWithoutZero失败: %v", err)
|
||||
}
|
||||
|
||||
expected := `{"activity_no":"ACT20260622001","out_biz_no":"order_001"}`
|
||||
if result != expected {
|
||||
t.Errorf("期望 %s, 得到 %s", expected, result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildSignStr(t *testing.T) {
|
||||
appID := "123456"
|
||||
timestamp := "2026-06-22 15:30:00"
|
||||
ciphertext := "encrypted_data"
|
||||
|
||||
signStr := BuildSignStr(appID, timestamp, ciphertext)
|
||||
expected := "1234562026-06-22 15:30:00encrypted_data"
|
||||
if signStr != expected {
|
||||
t.Errorf("期望 %s, 得到 %s", expected, signStr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateTimestamp(t *testing.T) {
|
||||
ts := GenerateTimestamp()
|
||||
// 验证格式 yyyy-MM-dd HH:mm:ss
|
||||
if len(ts) != 19 {
|
||||
t.Errorf("时间戳格式错误: %s", ts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDecryptAES(t *testing.T) {
|
||||
key := []byte("1234567890123456") // 16字节
|
||||
plaintext := `{"out_biz_no":"order_001","activity_no":"ACT001"}`
|
||||
|
||||
ciphertext, err := AESECBEncrypt([]byte(plaintext), key)
|
||||
if err != nil {
|
||||
t.Fatalf("加密失败: %v", err)
|
||||
}
|
||||
|
||||
decrypted, err := AESECBDecrypt(ciphertext, key)
|
||||
if err != nil {
|
||||
t.Fatalf("解密失败: %v", err)
|
||||
}
|
||||
|
||||
if string(decrypted) != plaintext {
|
||||
t.Errorf("解密结果不匹配: 期望 %s, 得到 %s", plaintext, string(decrypted))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptDecryptSM4(t *testing.T) {
|
||||
key := []byte("1234567890123456") // 16字节
|
||||
plaintext := `{"out_biz_no":"order_001","activity_no":"ACT001"}`
|
||||
|
||||
ciphertext, err := SM4CBCEncrypt([]byte(plaintext), key)
|
||||
if err != nil {
|
||||
t.Fatalf("SM4加密失败: %v", err)
|
||||
}
|
||||
|
||||
decrypted, err := SM4CBCDecrypt(ciphertext, key)
|
||||
if err != nil {
|
||||
t.Fatalf("SM4解密失败: %v", err)
|
||||
}
|
||||
|
||||
if string(decrypted) != plaintext {
|
||||
t.Errorf("SM4解密结果不匹配: 期望 %s, 得到 %s", plaintext, string(decrypted))
|
||||
}
|
||||
}
|
||||
|
||||
func TestEncryptBizParams(t *testing.T) {
|
||||
key := []byte("1234567890123456")
|
||||
req := KeyOrderRequest{
|
||||
OutBizNo: "order_001",
|
||||
ActivityNo: "ACT20260622001",
|
||||
}
|
||||
|
||||
ciphertext, err := EncryptBizParams(req, key, EncryptTypeAES)
|
||||
if err != nil {
|
||||
t.Fatalf("加密业务参数失败: %v", err)
|
||||
}
|
||||
|
||||
if ciphertext == "" {
|
||||
t.Fatal("加密结果为空")
|
||||
}
|
||||
|
||||
// 验证可以解密
|
||||
decrypted, err := DecryptBizParams(ciphertext, key, EncryptTypeAES)
|
||||
if err != nil {
|
||||
t.Fatalf("解密失败: %v", err)
|
||||
}
|
||||
|
||||
var result KeyOrderRequest
|
||||
if err := json.Unmarshal(decrypted, &result); err != nil {
|
||||
t.Fatalf("解析失败: %v", err)
|
||||
}
|
||||
|
||||
if result.OutBizNo != "order_001" {
|
||||
t.Errorf("OutBizNo不匹配: 期望 order_001, 得到 %s", result.OutBizNo)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
_, err := NewClient(&ClientConfig{
|
||||
AppID: testAppID,
|
||||
PrivateKey: testPrivateKey,
|
||||
PublicKey: testPublicKey,
|
||||
Key: testKey,
|
||||
EncryptType: EncryptTypeAES,
|
||||
Env: EnvTest,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("创建客户端失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewClientMissingFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *ClientConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{"空AppID", &ClientConfig{PrivateKey: "pk", PublicKey: "pub", Key: "key"}, true},
|
||||
{"空PrivateKey", &ClientConfig{AppID: "app", PublicKey: "pub", Key: "key"}, true},
|
||||
{"空PublicKey", &ClientConfig{AppID: "app", PrivateKey: "pk", Key: "key"}, true},
|
||||
{"空Key", &ClientConfig{AppID: "app", PrivateKey: "pk", PublicKey: "pub"}, true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := NewClient(tt.config)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("NewClient() error = %v, wantErr = %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAPIError(t *testing.T) {
|
||||
err := NewAPIError(401, "Signature verification failed", "INVALID_SIGNATURE")
|
||||
if err.Code != 401 {
|
||||
t.Errorf("Code不匹配: 期望 401, 得到 %d", err.Code)
|
||||
}
|
||||
if err.Message != "Signature verification failed" {
|
||||
t.Errorf("Message不匹配")
|
||||
}
|
||||
if err.Reason != "INVALID_SIGNATURE" {
|
||||
t.Errorf("Reason不匹配")
|
||||
}
|
||||
if err.Error() != "API error: code=401, message=Signature verification failed, reason=INVALID_SIGNATURE" {
|
||||
t.Errorf("Error()不匹配: %s", err.Error())
|
||||
}
|
||||
if err.IsSuccess() {
|
||||
t.Error("401应该不是成功")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSDKError(t *testing.T) {
|
||||
err := NewSDKError("测试错误", nil)
|
||||
if err.Error() != "SDK error: 测试错误" {
|
||||
t.Errorf("Error()不匹配: %s", err.Error())
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,311 @@
|
|||
## 文档概述
|
||||
- 接口总数:5个
|
||||
- 环境配置:
|
||||
- 测试环境地址:https://gateway.dev.cdlsxd.cn
|
||||
- 正式环境地址:https://market.api.86698.cn
|
||||
- 测试参数:
|
||||
- app_id: "xxx"
|
||||
- private_key: "xxx"(应用客户私钥,用于请求签名)
|
||||
- public_key: "xxx"(应用平台公钥,用于平台响应或回调验签)
|
||||
- key: "xxxx"(业务参数加密key)
|
||||
- activity_no: "xxxx"(活动编号)
|
||||
- sign_type: "RSA"
|
||||
|
||||
## 认证与安全
|
||||
### 公共Header请求参数
|
||||
| 字段名称 | 类型 | 描述 | 示例值 |
|
||||
|--------|------|------|--------|
|
||||
| Appid | string | 分配给开发者的应用 ID | 123456 |
|
||||
| Timestamp | string | 发送请求的时间,格式 yyyy-MM-dd HH:mm:ss | 2026-06-22 15:30:00 |
|
||||
| Sign | string | 请求签名串 | 详见 SDK 示例 |
|
||||
| Content-Type | string | 请求数据格式 | application/json |
|
||||
|
||||
### 公共请求参数
|
||||
| 字段名称 | 类型 | 描述 | 示例值 |
|
||||
|--------|------|------|--------|
|
||||
| ciphertext | string | 请求业务参数加密串 | 详见 SDK 示例 |
|
||||
|
||||
### 公共响应参数
|
||||
| 字段名称 | 类型 | 描述 |
|
||||
|--------|------|------|
|
||||
| code | int32 | 200 成功 |
|
||||
| message | string | 请求描述 |
|
||||
| reason | string | 错误原因,错误时返回 |
|
||||
| data.ciphertext | string | 业务响应加密串 |
|
||||
|
||||
### 业务参数加密规则
|
||||
1. 将业务参数去掉“零”值的参数,再由小到大按照字母排序,转成json字符串得到plaintext
|
||||
2. 使用应用key将plaintext字符串加密,支持两种模式:aes(ECB模式)/sm4(CBC模式),得到加密业务参数ciphertext
|
||||
|
||||
### 签名规则
|
||||
1. 拼接签名字符串:分配给开发者的应用ID + 发送请求的时间 + 加密业务参数
|
||||
2. 使用应用私钥将拼接待签名字符串生成签名字符串
|
||||
|
||||
### 回调验签规则
|
||||
1. 获取header头里面的签名信息
|
||||
2. 获取body里面的业务参数data
|
||||
3. 将业务参数data去掉“零”值的参数,再由小到大按照字母排序,转成json字符串得到plaintext
|
||||
4. 使用应用key将plaintext字符串加密[aes/sm4]得到加密得到ciphertext
|
||||
5. 拼接签名字符串:分配给开发者的应用ID + 发送请求的时间 + ciphertext
|
||||
6. 使用应用公钥验签
|
||||
|
||||
### 时间戳规则
|
||||
时间格式:yyyy-MM-dd HH:mm:ss,请求时间与服务端时间误差不能超过3分钟
|
||||
|
||||
## 接口列表
|
||||
### 接口 1:获取券码
|
||||
- 路径:/openapi/v1/key/order
|
||||
- 方法:POST
|
||||
- 描述:申请单个券码/权益,支持幂等
|
||||
|
||||
#### 请求参数(业务明文)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 是 | 外部业务号,幂等 |
|
||||
| activity_no | string | 是 | 活动编号 |
|
||||
| account | string | 否 | 账号,按活动类型透传 |
|
||||
| notify_url | string | 否 | 回调通知地址 |
|
||||
|
||||
#### 响应参数(解密后)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 是 | 外部业务号 |
|
||||
| trade_no | string | 是 | 交易号 |
|
||||
| key | string | 否 | 卡密 |
|
||||
| url | string | 否 | 链接型活动返回短链接,key/url 不会同时为空 |
|
||||
| valid_begin_time | string | 否 | 生效时间 |
|
||||
| valid_end_time | string | 否 | 失效时间 |
|
||||
| usable_num | uint32 | 是 | 可用次数 |
|
||||
| usage_num | uint32 | 是 | 已使用次数 |
|
||||
| status | uint32 | 是 | 状态:1 正常,2 已核销,3 已作废 |
|
||||
| settlement_price | float | 否 | 结算价 |
|
||||
| account | string | 否 | 上报账号 |
|
||||
|
||||
#### 示例
|
||||
- 明文业务参数:
|
||||
```json
|
||||
{
|
||||
"out_biz_no": "order_001",
|
||||
"activity_no": "ACT20260622001",
|
||||
"account": "18666666666",
|
||||
"notify_url": "https://notify.example.com/openapi"
|
||||
}
|
||||
```
|
||||
- 请求体示例:
|
||||
```json
|
||||
{
|
||||
"ciphertext": "加密后的业务报文"
|
||||
}
|
||||
```
|
||||
- 异常响应示例:
|
||||
```json
|
||||
{
|
||||
"code": 401,
|
||||
"message": "Signature verification failed",
|
||||
"reason": "INVALID_SIGNATURE"
|
||||
}
|
||||
```
|
||||
- 成功响应示例:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"ciphertext": "加密后的响应报文"
|
||||
},
|
||||
"message": "成功"
|
||||
}
|
||||
```
|
||||
- 解密后成功示例:
|
||||
```json
|
||||
{
|
||||
"out_biz_no": "order_001",
|
||||
"trade_no": "7251449503000383488",
|
||||
"key": "aZKdU9BymzR6qGRzJM",
|
||||
"url": "",
|
||||
"valid_begin_time": "2026-06-22 15:30:00",
|
||||
"valid_end_time": "2026-12-31 23:59:59",
|
||||
"usable_num": 1,
|
||||
"usage_num": 0,
|
||||
"status": 1,
|
||||
"settlement_price": 9.9,
|
||||
"account": "18666666666"
|
||||
}
|
||||
```
|
||||
|
||||
### 接口 2:券码查询
|
||||
- 路径:/openapi/v1/key/query
|
||||
- 方法:POST
|
||||
- 描述:查询已申请的券码详情
|
||||
|
||||
#### 请求参数(业务明文)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 否 | 外部业务号,与 trade_no 二选一 |
|
||||
| trade_no | string | 否 | 交易号,与 out_biz_no 二选一 |
|
||||
|
||||
#### 响应参数(解密后)
|
||||
与“获取券码”响应参数完全一致。
|
||||
|
||||
#### 示例
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"out_biz_no": "order_001",
|
||||
"trade_no": "7251449503000383488",
|
||||
"key": "aZKdU9BymzR6qGRzJM",
|
||||
"url": "",
|
||||
"valid_begin_time": "2026-06-22 15:30:00",
|
||||
"valid_end_time": "2026-12-31 23:59:59",
|
||||
"usable_num": 1,
|
||||
"usage_num": 0,
|
||||
"status": 1,
|
||||
"settlement_price": 9.9,
|
||||
"account": "18666666666",
|
||||
"ciphertext": ""
|
||||
},
|
||||
"message": "成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 接口 3:券码作废
|
||||
- 路径:/openapi/v1/key/discard
|
||||
- 方法:POST
|
||||
- 描述:作废已申请的券码
|
||||
|
||||
#### 请求参数(业务明文)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 否 | 外部业务号,与 trade_no 二选一 |
|
||||
| trade_no | string | 否 | 交易号,与 out_biz_no 二选一 |
|
||||
|
||||
#### 响应参数(解密后)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 是 | 外部业务号 |
|
||||
| trade_no | string | 是 | 交易号 |
|
||||
| status | uint32 | 是 | 3 表示已作废 |
|
||||
|
||||
#### 示例
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"out_biz_no": "order_001",
|
||||
"trade_no": "7251449503000383488",
|
||||
"status": 3,
|
||||
"ciphertext": ""
|
||||
},
|
||||
"message": "成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 接口 4:批量发卡
|
||||
- 路径:/openapi/v1/key/batch_order
|
||||
- 方法:POST
|
||||
- 描述:批量申请多个券码,返回异步任务状态
|
||||
|
||||
#### 请求参数(业务明文)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 是 | 外部业务号,幂等 |
|
||||
| activity_no | string | 是 | 活动编号 |
|
||||
| number | int32 | 是 | 发卡数量 |
|
||||
| notify_url | string | 否 | 回调通知地址 |
|
||||
|
||||
#### 响应参数(解密后)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 是 | 外部业务号 |
|
||||
| trade_no | string | 是 | 交易号 |
|
||||
| status | string | 是 | 任务状态,初始返回 processing |
|
||||
|
||||
#### 示例
|
||||
- 成功响应示例:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"ciphertext": "加密后的响应报文"
|
||||
},
|
||||
"message": "成功"
|
||||
}
|
||||
```
|
||||
- 解密后示例:
|
||||
```json
|
||||
{
|
||||
"out_biz_no": "batch_001",
|
||||
"trade_no": "7251449503000383499",
|
||||
"status": "processing"
|
||||
}
|
||||
```
|
||||
|
||||
### 接口 5:批量查询
|
||||
- 路径:/openapi/v1/key/batch_query
|
||||
- 方法:POST
|
||||
- 描述:查询批量发卡任务的状态和结果
|
||||
|
||||
#### 请求参数(业务明文)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 否 | 外部业务号,与 trade_no 二选一 |
|
||||
| trade_no | string | 否 | 交易号,与 out_biz_no 二选一 |
|
||||
|
||||
#### 响应参数(解密后)
|
||||
| 参数名 | 类型 | 必填 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| out_biz_no | string | 是 | 外部业务号 |
|
||||
| trade_no | string | 是 | 交易号 |
|
||||
| status | string | 是 | processing / success / failed |
|
||||
| download_url | string | 否 | 批量任务成功后返回下载地址 |
|
||||
| zip_password | string | 否 | 批量任务成功后返回压缩包密码 |
|
||||
|
||||
#### 示例
|
||||
- 成功响应示例:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"data": {
|
||||
"ciphertext": "加密后的响应报文"
|
||||
},
|
||||
"message": "成功"
|
||||
}
|
||||
```
|
||||
- 解密后示例:
|
||||
```json
|
||||
{
|
||||
"out_biz_no": "batch_001",
|
||||
"trade_no": "7251449503000383499",
|
||||
"status": "success",
|
||||
"download_url": "https://oss.example.com/openapi\_7251449503000383499.zip",
|
||||
"zip_password": "123456"
|
||||
}
|
||||
```
|
||||
|
||||
## 错误码
|
||||
| 错误码 | 说明 | 处理建议 |
|
||||
|--------|------|----------|
|
||||
| 500 | PANIC/其它 系统错误 | 联系平台处理 |
|
||||
| 400 | INVALID_PAYLOAD 请求外壳格式错误 | 请检查请求 JSON 结构 |
|
||||
| 400 | MISSING_PARAM 缺少必要参数 | 请检查 app_id、timestamp、sign、ciphertext |
|
||||
| 400 | INVALID_TIMESTAMP 时间格式错误 | 请检查时间格式 |
|
||||
| 400 | DECRYPT_FAILED 业务参数解密失败 | 请检查加密方式与密钥 |
|
||||
| 400 | PARAM_FAIL 参数错误 | 请检查业务参数 |
|
||||
| 400 | PARAM_DECRYPT_FAIL 明文参数格式错误 | 请检查密文解密后的业务报文 |
|
||||
| 401 | APP_NOT_FOUND 应用不存在 | 请检查应用 ID |
|
||||
| 401 | INVALID_SIGNATURE 签名错误 | 请检查签名串与私钥 |
|
||||
| 401 | EXPIRED_TIMESTAMP 请求已过期 | 请检查客户端时间 |
|
||||
| 401 | ACTIVITY_NOT_AUTH 活动未授权 | 请检查活动授权状态 |
|
||||
| 401 | MERCHANT_NOT_EXIST 客户不存在 | 请检查客户是否存在 |
|
||||
| 401 | MERCHANT_NOT_AUTH 客户冻结 | 请检查客户授权状态 |
|
||||
| 401 | MERCHANT_APP_INCOMPLETE 客户应用配置未完善 | 请检查客户应用配置 |
|
||||
| 401 | MERCHANT_APP_NOT_AUTH 应用不存在或未授权 | 请检查客户应用授权状态 |
|
||||
| 403 | ACTIVITY_EXPIRE 活动已结束 | 请检查活动是否有效 |
|
||||
| 403 | ACTIVITY_OUT_OF_STOCK 活动剩余量不足 | 请检查活动库存 |
|
||||
| 404 | ACTIVITY_NOT_EXIST 活动不存在 | 请检查活动编号 |
|
||||
| 404 | MERCHANT_ORDER_NOT_EXIST 订单不存在 | 请检查交易号或外部业务号 |
|
||||
| 404 | KEY_NOT_EXIST key码不存在 | 请检查订单信息 |
|
||||
| 429 | DUPLICATE_REQUEST 重复请求,请稍后重试 | 请避免短时间内重复提交同一业务号 |
|
||||
|
||||
## 回调通知
|
||||
平台会将异步任务结果推送到请求中指定的notify_url地址,回调验签规则参考上述认证与安全章节的回调验签流程。
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
package ymt_v3
|
||||
|
||||
// CommonResponse 公共响应结构
|
||||
type CommonResponse struct {
|
||||
Code int32 `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
Data *CommonData `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// CommonData 公共响应数据(包含加密的ciphertext)
|
||||
type CommonData struct {
|
||||
Ciphertext string `json:"ciphertext"`
|
||||
}
|
||||
|
||||
// KeyOrderRequest 获取券码请求业务参数
|
||||
type KeyOrderRequest struct {
|
||||
OutBizNo string `json:"out_biz_no"`
|
||||
ActivityNo string `json:"activity_no"`
|
||||
Account string `json:"account,omitempty"`
|
||||
NotifyURL string `json:"notify_url,omitempty"`
|
||||
}
|
||||
|
||||
// KeyOrderResponse 获取券码/券码查询响应业务参数
|
||||
type KeyOrderResponse struct {
|
||||
OutBizNo string `json:"out_biz_no"`
|
||||
TradeNo string `json:"trade_no"`
|
||||
Key string `json:"key,omitempty"`
|
||||
URL string `json:"url,omitempty"`
|
||||
ValidBeginTime string `json:"valid_begin_time,omitempty"`
|
||||
ValidEndTime string `json:"valid_end_time,omitempty"`
|
||||
UsableNum uint32 `json:"usable_num"`
|
||||
UsageNum uint32 `json:"usage_num"`
|
||||
Status uint32 `json:"status"`
|
||||
SettlementPrice float64 `json:"settlement_price,omitempty"`
|
||||
Account string `json:"account,omitempty"`
|
||||
}
|
||||
|
||||
// KeyQueryRequest 券码查询请求业务参数
|
||||
type KeyQueryRequest struct {
|
||||
OutBizNo string `json:"out_biz_no,omitempty"`
|
||||
TradeNo string `json:"trade_no,omitempty"`
|
||||
}
|
||||
|
||||
// KeyDiscardRequest 券码作废请求业务参数
|
||||
type KeyDiscardRequest struct {
|
||||
OutBizNo string `json:"out_biz_no,omitempty"`
|
||||
TradeNo string `json:"trade_no,omitempty"`
|
||||
}
|
||||
|
||||
// KeyDiscardResponse 券码作废响应业务参数
|
||||
type KeyDiscardResponse struct {
|
||||
OutBizNo string `json:"out_biz_no"`
|
||||
TradeNo string `json:"trade_no"`
|
||||
Status uint32 `json:"status"`
|
||||
}
|
||||
|
||||
// BatchOrderRequest 批量发卡请求业务参数
|
||||
type BatchOrderRequest struct {
|
||||
OutBizNo string `json:"out_biz_no"`
|
||||
ActivityNo string `json:"activity_no"`
|
||||
Number int32 `json:"number"`
|
||||
NotifyURL string `json:"notify_url,omitempty"`
|
||||
}
|
||||
|
||||
// BatchOrderResponse 批量发卡响应业务参数
|
||||
type BatchOrderResponse struct {
|
||||
OutBizNo string `json:"out_biz_no"`
|
||||
TradeNo string `json:"trade_no"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
// BatchQueryRequest 批量查询请求业务参数
|
||||
type BatchQueryRequest struct {
|
||||
OutBizNo string `json:"out_biz_no,omitempty"`
|
||||
TradeNo string `json:"trade_no,omitempty"`
|
||||
}
|
||||
|
||||
// BatchQueryResponse 批量查询响应业务参数
|
||||
type BatchQueryResponse struct {
|
||||
OutBizNo string `json:"out_biz_no"`
|
||||
TradeNo string `json:"trade_no"`
|
||||
Status string `json:"status"`
|
||||
DownloadURL string `json:"download_url,omitempty"`
|
||||
ZipPassword string `json:"zip_password,omitempty"`
|
||||
}
|
||||
|
||||
// NotifyPayload 回调通知的body结构
|
||||
type NotifyPayload struct {
|
||||
Data *CommonData `json:"data"`
|
||||
}
|
||||
|
||||
// NotifyHeader 回调通知的header
|
||||
type NotifyHeader struct {
|
||||
Appid string `json:"Appid"`
|
||||
Timestamp string `json:"Timestamp"`
|
||||
Sign string `json:"Sign"`
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue