This commit is contained in:
parent
ceba8ce43d
commit
9d3bfd07ff
|
|
@ -44,6 +44,4 @@ ENV TZ=Asia/Shanghai
|
||||||
|
|
||||||
|
|
||||||
CMD ["./server"]
|
CMD ["./server"]
|
||||||
# 不被他人看法左右
|
|
||||||
# 纸上谈兵是免费的
|
|
||||||
# 《拿来》
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,56 @@
|
||||||
package test
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strconv"
|
"strconv"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
"xy_sh/internal/biz"
|
||||||
|
"xy_sh/internal/config"
|
||||||
|
"xy_sh/internal/data/impl"
|
||||||
"xy_sh/internal/entities"
|
"xy_sh/internal/entities"
|
||||||
"xy_sh/internal/router"
|
"xy_sh/internal/router"
|
||||||
|
service2 "xy_sh/internal/service"
|
||||||
"xy_sh/pkg/crypto"
|
"xy_sh/pkg/crypto"
|
||||||
|
"xy_sh/utils"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const URL = "http://127.0.0.1:8085"
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
crypto.InitCrypto("test_salt_12345", "test_key_16bytes")
|
crypto.InitCrypto("test_salt_12345", "test_key_16bytes")
|
||||||
}
|
}
|
||||||
|
|
||||||
func setupTestApp() *fiber.App {
|
func setupTestApp() *fiber.App {
|
||||||
app := fiber.New()
|
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
|
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("timestamp", timestamp)
|
||||||
req.Header.Set("sign", sign)
|
req.Header.Set("sign", sign)
|
||||||
|
|
||||||
resp, err := app.Test(req)
|
resp, err := app.Test(req, 50000)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("请求失败: %v", err)
|
t.Fatalf("请求失败: %v", err)
|
||||||
}
|
}
|
||||||
|
|
@ -65,11 +92,24 @@ func makeEncryptedRequest(t *testing.T, app *fiber.App, path string, bizData int
|
||||||
return resp, body
|
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) {
|
func TestCreateOrder(t *testing.T) {
|
||||||
app := setupTestApp()
|
app := setupTestApp()
|
||||||
|
|
||||||
bizData := entities.OrderRequest{
|
bizData := entities.OrderRequest{
|
||||||
ActCode: "ACT001",
|
ActCode: "ACT007",
|
||||||
GoodsCode: "123456",
|
GoodsCode: "123456",
|
||||||
ActOrderNum: "TEST_ORDER_001",
|
ActOrderNum: "TEST_ORDER_001",
|
||||||
Account: "19912345678",
|
Account: "19912345678",
|
||||||
|
|
@ -97,38 +137,9 @@ func TestCreateOrder(t *testing.T) {
|
||||||
func TestQueryOrder(t *testing.T) {
|
func TestQueryOrder(t *testing.T) {
|
||||||
app := setupTestApp()
|
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{
|
queryBizData := entities.QueryOrderRequest{
|
||||||
ActCode: "ACT001",
|
//ActCode: "ACT007",
|
||||||
OrderNo: orderResp.OrderNo,
|
OrderNo: "936312532119846913",
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, body := makeEncryptedRequest(t, app, "/api/v1/order/query", queryBizData)
|
resp, body := makeEncryptedRequest(t, app, "/api/v1/order/query", queryBizData)
|
||||||
|
|
@ -238,11 +249,11 @@ func TestHealthCheck(t *testing.T) {
|
||||||
app := setupTestApp()
|
app := setupTestApp()
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||||
resp, body := app.Test(req)
|
resp, err := app.Test(req)
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
t.Errorf("期望状态码200,实际: %d", resp.StatusCode)
|
t.Errorf("期望状态码200,实际: %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.Logf("健康检查接口测试成功,响应: %s", string(body))
|
t.Logf("健康检查接口测试成功,响应: %s", err)
|
||||||
}
|
}
|
||||||
|
|
@ -1,18 +1,29 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
"log"
|
"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/config"
|
||||||
"xy_sh/internal/router"
|
"xy_sh/internal/router"
|
||||||
"xy_sh/pkg/crypto"
|
"xy_sh/pkg/crypto"
|
||||||
|
|
||||||
|
"github.com/go-viper/mapstructure/v2"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
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)
|
crypto.InitCrypto(cfg.SM3Salt, cfg.SM4Key)
|
||||||
|
|
||||||
app := fiber.New(fiber.Config{
|
app := fiber.New(fiber.Config{
|
||||||
|
|
@ -21,10 +32,31 @@ func main() {
|
||||||
WriteTimeout: cfg.WriteTimeout,
|
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)
|
log.Printf("服务启动,监听端口: %s", cfg.ServerPort)
|
||||||
if err := app.Listen(":" + cfg.ServerPort); err != nil {
|
if err := app.Listen(":" + cfg.ServerPort); err != nil {
|
||||||
log.Fatalf("服务启动失败: %v", err)
|
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}
|
||||||
36
xy_sh/go.mod
36
xy_sh/go.mod
|
|
@ -3,6 +3,42 @@ module xy_sh
|
||||||
go 1.26
|
go 1.26
|
||||||
|
|
||||||
require (
|
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/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
|
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
|
package biz
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"context"
|
||||||
"encoding/hex"
|
"encoding/json"
|
||||||
"errors"
|
"fmt"
|
||||||
"io"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"strings"
|
||||||
"time"
|
"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"
|
"xy_sh/internal/entities"
|
||||||
)
|
)
|
||||||
|
|
||||||
type OrderStore struct {
|
type OrderStore struct {
|
||||||
mu sync.RWMutex
|
orders *impl.OrderImpl
|
||||||
orders map[string]*OrderInfo
|
ymtClient *ymt_v3.Client
|
||||||
|
cfg *config.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
type OrderInfo struct {
|
func NewOrderStore(orders *impl.OrderImpl, ymtClient *ymt_v3.Client, cfg *config.Config) *OrderStore {
|
||||||
OrderNo string
|
return &OrderStore{
|
||||||
ActOrderNum string
|
orders: orders,
|
||||||
GoodsCode string
|
ymtClient: ymtClient,
|
||||||
ActCode string
|
cfg: cfg,
|
||||||
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 CreateOrder(req *entities.OrderRequest) (*entities.OrderResponseData, error) {
|
func (o *OrderStore) CreateOrder(ctx context.Context, req *entities.OrderRequest) (*entities.OrderResponseData, error) {
|
||||||
store.mu.Lock()
|
var orderInfo model.XyShOrder
|
||||||
defer store.mu.Unlock()
|
err := o.orders.GetByKey(ctx, "act_order_num", req.ActOrderNum, &orderInfo)
|
||||||
|
if err != nil {
|
||||||
orderNo := generateOrderNo()
|
return nil, err
|
||||||
|
}
|
||||||
orderInfo := &OrderInfo{
|
if orderInfo.ID == 0 {
|
||||||
OrderNo: orderNo,
|
orderInfo = model.XyShOrder{
|
||||||
ActOrderNum: req.ActOrderNum,
|
ActOrderNum: req.ActOrderNum,
|
||||||
GoodsCode: req.GoodsCode,
|
GoodsCode: req.GoodsCode,
|
||||||
ActCode: req.ActCode,
|
ActCode: req.ActCode,
|
||||||
Account: req.Account,
|
Account: req.Account,
|
||||||
Status: entities.StatusSuccess,
|
Status: entities.StatusSuccess,
|
||||||
CreateTime: time.Now(),
|
CallBackURL: req.CallbackUrl,
|
||||||
UpdateTime: time.Now(),
|
CreatedAt: time.Now(),
|
||||||
|
UpdatedAt: time.Now(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
orderInfo.CouponNo = generateCouponNo()
|
selfPublicIP, err := pkg.GetPublicIP()
|
||||||
orderInfo.CouponCode = orderInfo.CouponNo
|
if err != nil {
|
||||||
orderInfo.ExpireTime = time.Now().AddDate(1, 0, 0).Format("2006-01-02 15:04:05")
|
selfPublicIP = "120.55.12.245"
|
||||||
|
}
|
||||||
store.orders[orderNo] = orderInfo
|
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{
|
return &entities.OrderResponseData{
|
||||||
OrderNo: orderNo,
|
OrderNo: result.TradeNo,
|
||||||
CouponNo: orderInfo.CouponNo,
|
CouponNo: result.URL,
|
||||||
CouponCode: orderInfo.CouponCode,
|
CouponCode: result.Key,
|
||||||
Status: orderInfo.Status,
|
Status: o.getStatus(result.Status, result.ValidEndTime, err),
|
||||||
ExpireTime: orderInfo.ExpireTime,
|
ExpireTime: result.ValidEndTime,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func QueryOrder(req *entities.QueryOrderRequest) (*entities.QueryOrderResponseData, error) {
|
func (o *OrderStore) getStatus(ymtStatus uint32, ymtValidTime string, err error) int {
|
||||||
store.mu.RLock()
|
if err != nil {
|
||||||
defer store.mu.RUnlock()
|
return entities.StatusFailed
|
||||||
|
|
||||||
orderInfo, exists := store.orders[req.OrderNo]
|
|
||||||
if !exists {
|
|
||||||
return nil, errors.New("订单不存在")
|
|
||||||
}
|
}
|
||||||
|
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{
|
resp := &entities.QueryOrderResponseData{
|
||||||
OrderNo: orderInfo.OrderNo,
|
OrderNo: result.TradeNo,
|
||||||
Status: orderInfo.Status,
|
Status: o.getStatus(result.Status, result.ValidEndTime, err),
|
||||||
Account: orderInfo.Account,
|
Account: result.Account,
|
||||||
CouponId: orderInfo.CouponId,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if orderInfo.CouponNo != "" {
|
if result.URL != "" {
|
||||||
resp.CardInfo = &entities.CardInfo{
|
resp.CardInfo = &entities.CardInfo{
|
||||||
CouponNo: orderInfo.CouponNo,
|
CouponNo: result.URL,
|
||||||
CouponCode: orderInfo.CouponCode,
|
CouponCode: result.Key,
|
||||||
ExpireTime: orderInfo.ExpireTime,
|
ExpireTime: result.ValidEndTime,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func WxRecharge(req *entities.WxRechargeRequest) (*entities.WxRechargeResponseData, error) {
|
func (o *OrderStore) HandleCallback(req *entities.CallBack) error {
|
||||||
store.mu.Lock()
|
callBackData := &entities.CallbackRequest{
|
||||||
defer store.mu.Unlock()
|
OrderNo: req.Data.TradeNo,
|
||||||
|
ActOrderNum: req.Data.OutBizNo,
|
||||||
if orderInfo, exists := store.orders[req.OrderNo]; exists {
|
Status: o.getStatus(req.Data.Status, req.Data.ValidEndTime, nil),
|
||||||
return &entities.WxRechargeResponseData{
|
Account: req.Data.Account,
|
||||||
OrderNo: orderInfo.OrderNo,
|
CardInfo: &entities.CardInfo{
|
||||||
Status: orderInfo.Status,
|
CouponNo: req.Data.Url,
|
||||||
CouponId: orderInfo.CouponId,
|
CouponCode: req.Data.Key,
|
||||||
}, nil
|
ExpireTime: req.Data.ValidEndTime,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
sign, jsonData, err := makeEncryptedRequest(callBackData)
|
||||||
orderNo := req.OrderNo
|
if err != nil {
|
||||||
|
return err
|
||||||
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(),
|
|
||||||
}
|
}
|
||||||
|
requset := &l_request.Request{
|
||||||
store.orders[orderNo] = orderInfo
|
Method: http.MethodPost,
|
||||||
|
Json: jsonData,
|
||||||
return &entities.WxRechargeResponseData{
|
Url: o.cfg.NotifyUrl,
|
||||||
OrderNo: orderNo,
|
Headers: map[string]string{
|
||||||
Status: orderInfo.Status,
|
"Content-Type": "application/json",
|
||||||
CouponId: orderInfo.CouponId,
|
"timestamp": fmt.Sprintf("%d", time.Now().UnixMilli()),
|
||||||
}, nil
|
"sign": sign,
|
||||||
}
|
},
|
||||||
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
res, err := requset.Send()
|
||||||
orderInfo.Status = req.Status
|
if err != nil {
|
||||||
orderInfo.Account = req.Account
|
return err
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
if strings.ToLower(res.Text) != "ok" {
|
||||||
|
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func generateOrderNo() string {
|
func makeEncryptedRequest(bizData interface{}) (string, map[string]interface{}, error) {
|
||||||
return "HM" + strconv.FormatInt(time.Now().UnixMilli(), 10) + randomString(16)
|
bizJSON, err := json.Marshal(bizData)
|
||||||
}
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("业务数据序列化失败: %v", err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
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
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ServerPort string
|
ServerPort string `mapstructure:"server_port"`
|
||||||
SM3Salt string
|
SM3Salt string `mapstructure:"s_m3_salt"`
|
||||||
SM4Key string
|
SM4Key string `mapstructure:"sm4_key"`
|
||||||
|
NotifyUrl string `mapstructure:"notify_url"`
|
||||||
|
Ymt Ymt `mapstructure:"ymt"`
|
||||||
|
DB DB `mapstructure:"db"`
|
||||||
ReadTimeout time.Duration
|
ReadTimeout time.Duration
|
||||||
WriteTimeout time.Duration
|
WriteTimeout time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadConfig() *Config {
|
type Ymt struct {
|
||||||
port := getEnv("SERVER_PORT", "8080")
|
AppID string `mapstructure:"app_id"`
|
||||||
sm3Salt := getEnv("SM3_SALT", "default_salt_change_me")
|
PrivateKey string `mapstructure:"private_key"`
|
||||||
sm4Key := getEnv("SM4_KEY", "default_key_16by")
|
PublicKey string `mapstructure:"public_key"`
|
||||||
|
Key string `mapstructure:"key"`
|
||||||
return &Config{
|
ActivityNo string `mapstructure:"activity_no"`
|
||||||
ServerPort: port,
|
EncryptType string `mapstructure:"encrypt_type"`
|
||||||
SM3Salt: sm3Salt,
|
Env string `mapstructure:"env"`
|
||||||
SM4Key: sm4Key,
|
|
||||||
ReadTimeout: 30 * time.Second,
|
|
||||||
WriteTimeout: 30 * time.Second,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getEnv(key, defaultValue string) string {
|
type DB struct {
|
||||||
if value := os.Getenv(key); value != "" {
|
Driver string `mapstructure:"driver"`
|
||||||
return value
|
Source string `mapstructure:"source"`
|
||||||
}
|
MaxIdle int32 `mapstructure:"maxIdle"`
|
||||||
return defaultValue
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析配置
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
@ -41,3 +41,16 @@ type CardInfo struct {
|
||||||
CouponCode string `json:"couponCode"`
|
CouponCode string `json:"couponCode"`
|
||||||
ExpireTime string `json:"expireTime"`
|
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 (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"xy_sh/tmpl/errcode"
|
||||||
|
|
||||||
"xy_sh/internal/entities"
|
"xy_sh/internal/entities"
|
||||||
"xy_sh/pkg/crypto"
|
"xy_sh/pkg/crypto"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SignVerifyMiddleware() fiber.Handler {
|
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 {
|
return func(c *fiber.Ctx) error {
|
||||||
timestamp := c.Get("timestamp")
|
timestamp := c.Get("timestamp")
|
||||||
sign := c.Get("sign")
|
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 {
|
func EncryptResponseMiddleware() fiber.Handler {
|
||||||
return func(c *fiber.Ctx) error {
|
return func(c *fiber.Ctx) error {
|
||||||
err := c.Next()
|
err := c.Next()
|
||||||
|
if c.Path() == "/api/v1/callback/notify" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var resp *Res
|
||||||
|
// 如果有错误发生
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
// 返回自定义错误响应
|
||||||
}
|
resp = &Res{
|
||||||
|
Msg: err.Error(),
|
||||||
body := c.Response().Body()
|
Code: entities.CodeFailed,
|
||||||
if len(body) == 0 {
|
Data: nil,
|
||||||
return nil
|
}
|
||||||
}
|
} else {
|
||||||
|
body := c.Response().Body()
|
||||||
var resp entities.CommonResponse
|
if len(body) == 0 {
|
||||||
if err := json.Unmarshal(body, &resp); err != nil {
|
return nil
|
||||||
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 {
|
if resp.Data != nil {
|
||||||
dataBytes, err := json.Marshal(resp.Data)
|
dataBytes, err := json.Marshal(resp.Data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -103,7 +164,7 @@ func EncryptResponseMiddleware() fiber.Handler {
|
||||||
|
|
||||||
resp.Data = encryptedData
|
resp.Data = encryptedData
|
||||||
|
|
||||||
newBody, err := json.Marshal(resp)
|
newBody, err = json.Marshal(resp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(entities.CommonResponse{
|
return c.Status(fiber.StatusInternalServerError).JSON(entities.CommonResponse{
|
||||||
Code: entities.CodeFailed,
|
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
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,38 +1,81 @@
|
||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"xy_sh/internal/entities"
|
||||||
"xy_sh/internal/middleware"
|
"xy_sh/internal/middleware"
|
||||||
"xy_sh/internal/service"
|
"xy_sh/internal/service"
|
||||||
|
"xy_sh/pkg/crypto"
|
||||||
|
"xy_sh/tmpl/errcode"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/gofiber/fiber/v2/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func SetupRoutes(app *fiber.App) {
|
func SetupRoutes(
|
||||||
app.Get("/health", service.HealthHandler)
|
app *fiber.App,
|
||||||
app.Get("/", service.HealthHandler)
|
service *service.Order,
|
||||||
|
) {
|
||||||
v1 := app.Group("/api/v1")
|
|
||||||
|
|
||||||
|
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",
|
v1.Post("/order/create",
|
||||||
middleware.SignVerifyMiddleware(),
|
middleware.SignVerifyMiddleware(),
|
||||||
middleware.EncryptResponseMiddleware(),
|
|
||||||
service.CreateOrderHandler,
|
service.CreateOrderHandler,
|
||||||
)
|
)
|
||||||
|
|
||||||
v1.Post("/order/query",
|
v1.Post("/order/query",
|
||||||
middleware.SignVerifyMiddleware(),
|
middleware.SignVerifyMiddleware(),
|
||||||
middleware.EncryptResponseMiddleware(),
|
|
||||||
service.QueryOrderHandler,
|
service.QueryOrderHandler,
|
||||||
)
|
)
|
||||||
|
|
||||||
v1.Post("/wx/recharge",
|
|
||||||
middleware.SignVerifyMiddleware(),
|
|
||||||
middleware.EncryptResponseMiddleware(),
|
|
||||||
service.WxRechargeHandler,
|
|
||||||
)
|
|
||||||
|
|
||||||
v1.Post("/callback/notify",
|
v1.Post("/callback/notify",
|
||||||
middleware.SignVerifyMiddleware(),
|
|
||||||
service.CallbackHandler,
|
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 (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"xy_sh/internal/data/impl"
|
||||||
|
"xy_sh/pkg"
|
||||||
|
|
||||||
"xy_sh/internal/biz"
|
"xy_sh/internal/biz"
|
||||||
"xy_sh/internal/entities"
|
"xy_sh/internal/entities"
|
||||||
|
|
@ -9,63 +12,57 @@ import (
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
func CreateOrderHandler(c *fiber.Ctx) error {
|
type Order struct {
|
||||||
decryptedData := c.Locals("decryptedData").([]byte)
|
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
|
var req entities.OrderRequest
|
||||||
if err := json.Unmarshal(decryptedData, &req); err != nil {
|
if err := json.Unmarshal(decryptedData, &req); err != nil {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{
|
return fmt.Errorf("业务参数解析失败: %w", err)
|
||||||
Code: entities.CodeFailed,
|
|
||||||
Msg: "业务参数解析失败: " + err.Error(),
|
|
||||||
Data: nil,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.ActCode == "" || req.GoodsCode == "" || req.ActOrderNum == "" {
|
if req.ActCode == "" || req.GoodsCode == "" || req.ActOrderNum == "" {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{
|
return fmt.Errorf("缺少必填参数: actCode, goodsCode, actOrderNum")
|
||||||
Code: entities.CodeFailed,
|
|
||||||
Msg: "缺少必填参数: actCode, goodsCode, actOrderNum",
|
|
||||||
Data: nil,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := biz.CreateOrder(&req)
|
result, err := o.biz.CreateOrder(c.UserContext(), &req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusOK).JSON(entities.CommonResponse{
|
return err
|
||||||
Code: entities.CodeFailed,
|
|
||||||
Msg: err.Error(),
|
|
||||||
Data: nil,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).JSON(entities.CommonResponse{
|
return pkg.HandleResponse(c, result)
|
||||||
Code: entities.CodeSuccess,
|
|
||||||
Msg: "请求成功",
|
|
||||||
Data: result,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func QueryOrderHandler(c *fiber.Ctx) error {
|
func (o *Order) QueryOrderHandler(c *fiber.Ctx) error {
|
||||||
decryptedData := c.Locals("decryptedData").([]byte)
|
decryptedData := c.Locals("decryptedData").(string)
|
||||||
|
if len(decryptedData) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
var req entities.QueryOrderRequest
|
var req entities.QueryOrderRequest
|
||||||
if err := json.Unmarshal(decryptedData, &req); err != nil {
|
if err := json.Unmarshal([]byte(decryptedData), &req); err != nil {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{
|
return fmt.Errorf("业务参数解析失败: " + err.Error())
|
||||||
Code: entities.CodeFailed,
|
|
||||||
Msg: "业务参数解析失败: " + err.Error(),
|
|
||||||
Data: nil,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.ActCode == "" || req.OrderNo == "" {
|
if req.ActCode == "" && req.OrderNo == "" {
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(entities.CommonResponse{
|
return fmt.Errorf("缺少必填参数: actCode, orderNo")
|
||||||
Code: entities.CodeFailed,
|
|
||||||
Msg: "缺少必填参数: actCode, orderNo",
|
|
||||||
Data: nil,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := biz.QueryOrder(&req)
|
result, err := o.biz.QueryOrder(&req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return c.Status(fiber.StatusOK).JSON(entities.CommonResponse{
|
return c.Status(fiber.StatusOK).JSON(entities.CommonResponse{
|
||||||
Code: entities.CodeFailed,
|
Code: entities.CodeFailed,
|
||||||
|
|
@ -73,73 +70,16 @@ func QueryOrderHandler(c *fiber.Ctx) error {
|
||||||
Data: nil,
|
Data: nil,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return pkg.HandleResponse(c, result)
|
||||||
return c.Status(fiber.StatusOK).JSON(entities.CommonResponse{
|
|
||||||
Code: entities.CodeSuccess,
|
|
||||||
Msg: "请求成功",
|
|
||||||
Data: result,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func WxRechargeHandler(c *fiber.Ctx) error {
|
func (o *Order) CallbackHandler(c *fiber.Ctx) error {
|
||||||
decryptedData := c.Locals("decryptedData").([]byte)
|
|
||||||
|
|
||||||
var req entities.WxRechargeRequest
|
var req entities.CallBack
|
||||||
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 req.ActCode == "" || req.OrderNo == "" || req.GoodsCode == "" || req.ActOrderNum == "" ||
|
if err := o.biz.HandleCallback(&req); err != nil {
|
||||||
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 {
|
|
||||||
return c.Status(fiber.StatusInternalServerError).SendString("处理失败")
|
return c.Status(fiber.StatusInternalServerError).SendString("处理失败")
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).SendString("ok")
|
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