This commit is contained in:
renzhiyuan 2026-05-29 14:18:26 +08:00
commit 98a8e4c681
8 changed files with 409 additions and 0 deletions

8
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

64
entity.go Normal file
View File

@ -0,0 +1,64 @@
package l_short_utl_request
import "encoding/json"
type ResCommon struct {
Code int `json:"code"`
Message string `json:"message"`
Data json.RawMessage `json:"data"`
}
type ShortUrlBatchCreate struct {
//批次名称
BatchName string `json:"batch_name,omitempty"`
//链接
Urls []string `json:"urls"`
// 开始时间yyyy-mm-dd
StartTimeStr string `json:"start_time_str,omitempty"`
// 结束时间yyyy-mm-dd不填则永久有效
EndTimeStr string `json:"end_time_str,omitempty"`
//返回映射列表
WithBatchMap bool `json:"with_batch_map,omitempty"`
}
type ShortUrlBatchCreateResult struct {
Id int32 `json:"id"`
// 名称
BatchName string `json:"batch_name"`
// 批次号
BatchNo int64 `json:"batch_no"`
UrlMap map[string]string `json:"url_map,omitempty"`
}
type ShortUrlCreateReq struct {
BatchName string `json:"batch_name,omitempty"`
// 用户id
UserId int32 `json:"user_id,omitempty"`
// 链接
Urls []string `json:"urls,omitempty"`
// 开始时间yyyy-mm-dd
StartTimeStr string `json:"start_time_str,omitempty"`
// 结束时间yyyy-mm-dd不填则永久有效
EndTimeStr string `json:"end_time_str,omitempty"`
// 结束时间yyyy-mm-dd不填则永久有效
WithBatchMap bool `json:"with_batch_map,omitempty"`
}
type BatchQueryReq struct {
BatchNo int64 `json:"batch_no,omitempty"`
Id int64 `json:"id,omitempty"`
BatchName string `json:"batch_name,omitempty"`
OriginUrl string `json:"origin_url,omitempty"`
// 短链接
ShortUrl string `json:"short_url,omitempty"`
}
type QueryBatchWithUrlResp struct {
// id
Id int32 `json:"id,omitempty"`
// 名称
BatchName string `json:"batch_name,omitempty"`
// 批次号
BatchNo int64 `json:"batch_no,omitempty"`
UrlMap map[string]string `json:"url_map,omitempty"`
}

31
func.go Normal file
View File

@ -0,0 +1,31 @@
package l_short_utl_request
import (
"encoding/json"
"math/rand/v2"
)
// 纯小写字母
func GenerateRandomLowerString(n int) string {
return GenerateRandomStringCustom(n, "abcdefghijklmnopqrstuvwxyz")
}
// GenerateRandomStringCustom 使用自定义字符集
func GenerateRandomStringCustom(n int, charset string) string {
result := make([]byte, n)
for i := range result {
result[i] = charset[rand.IntN(len(charset))]
}
return string(result)
}
// 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
}

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module l_short_utl_request
go 1.26.2

18
option.go Normal file
View File

@ -0,0 +1,18 @@
package l_short_utl_request
import "fmt"
type Option func(*ShortUrl)
// WithHost 修改请求地址
func WithHost(host string) Option {
return func(b *ShortUrl) {
b.host = host
}
}
func WithAuth(auth string) Option {
return func(b *ShortUrl) {
b.auth = fmt.Sprintf("Basic %s", auth)
}
}

130
request.go Normal file
View File

@ -0,0 +1,130 @@
package l_short_utl_request
import (
"encoding/json"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// 请求结构体
type Request struct {
Method string `json:"method"` // 请求方法
Url string `json:"url"` // 请求url
Params map[string]string `json:"params"` // Query参数
Headers map[string]string `json:"headers"` // 请求头
Cookies map[string]string `json:"cookies"` // todo 处理 Cookies
Data map[string]string `json:"data"` // 表单格式请求数据
Json map[string]interface{} `json:"json"` // JSON格式请求数据 todo 多层 嵌套
Files map[string]string `json:"files"` // todo 处理 Files
Raw string `json:"raw"` // 原始请求数据
JsonByte []byte `json:"json_raw"` // JSON格式请求数据 todo 多层 嵌套
}
// 响应结构体
type Response struct {
StatusCode int `json:"status_code"` // 状态码
Reason string `json:"reason"` // 状态码说明
Elapsed float64 `json:"elapsed"` // 请求耗时(秒)
Content []byte `json:"content"` // 响应二进制内容
Text string `json:"text"` // 响应文本
Headers map[string]string `json:"headers"` // 响应头
Cookies map[string]string `json:"cookies"` // todo 添加响应Cookies
Request *Request `json:"request"` // 原始请求
}
// 处理请求方法
func (r *Request) getMethod() string {
return strings.ToUpper(r.Method) // 必须转为全部大写
}
// 组装URL
func (r *Request) getUrl() string {
if r.Params != nil {
urlValues := url.Values{}
Url, _ := url.Parse(r.Url) // todo 处理err
for key, value := range r.Params {
urlValues.Set(key, value)
}
Url.RawQuery = urlValues.Encode()
return Url.String()
}
return r.Url
}
// 组装请求数据
func (r *Request) getData() io.Reader {
var reqBody string
if r.Headers == nil {
r.Headers = make(map[string]string, 1)
}
if r.Raw != "" {
reqBody = r.Raw
} else if r.Data != nil {
urlValues := url.Values{}
for key, value := range r.Data {
urlValues.Add(key, value)
}
reqBody = urlValues.Encode()
r.Headers["Content-Type"] = "application/x-www-form-urlencoded"
} else if r.Json != nil {
bytesData, _ := json.Marshal(r.Json)
reqBody = string(bytesData)
r.Headers["Content-Type"] = "application/json"
} else if r.JsonByte != nil {
reqBody = string(r.JsonByte)
r.Headers["Content-Type"] = "application/json"
}
return strings.NewReader(reqBody)
}
// 添加请求头-需要在getData后使用
func (r *Request) addHeaders(req *http.Request) {
if r.Headers != nil {
for key, value := range r.Headers {
req.Header.Add(key, value)
}
}
}
// 准备请求
func (r *Request) prepare() *http.Request {
Method := r.getMethod()
Url := r.getUrl()
Data := r.getData()
req, _ := http.NewRequest(Method, Url, Data)
r.addHeaders(req)
return req
}
// 组装响应对象
func (r *Request) packResponse(res *http.Response, elapsed float64) Response {
var resp Response
resBody, _ := io.ReadAll(res.Body)
resp.Content = resBody
resp.Text = string(resBody)
resp.StatusCode = res.StatusCode
resp.Reason = strings.Split(res.Status, " ")[1]
resp.Elapsed = elapsed
resp.Headers = map[string]string{}
for key, value := range res.Header {
resp.Headers[key] = strings.Join(value, ";")
}
return resp
}
// 发送请求
func (r *Request) Send() (Response, error) {
req := r.prepare()
client := &http.Client{}
start := time.Now()
res, err := client.Do(req)
if err != nil {
return Response{}, err
}
defer res.Body.Close()
elapsed := time.Since(start).Seconds()
return r.packResponse(res, elapsed), nil
}

119
short_url.go Normal file
View File

@ -0,0 +1,119 @@
package l_short_utl_request
import (
"encoding/json"
"errors"
"fmt"
"time"
)
type ShortUrl struct {
host string
auth string
}
func NewClient(args ...Option) *ShortUrl {
s := &ShortUrl{
host: "https://crm.1688sup.com/api",
}
for _, opt := range args {
opt(s)
}
return s
}
// BatchCreate 批量创建短链接。
//
// 参数:
// - urls: 需要生成短链的原始 URL 列表,每个 URL 长度至少为 1不能为空切片。
// - index: 外部索引标识,用于后续批量查询和分组管理。非必填,传空字符串表示不设置索引。
//
// 返回:
// - error: 创建过程中遇到的错误,如参数校验失败、存储异常等。
//
// 注意:该方法会为每个 URL 生成对应的短链映射关系,任一 URL 创建失败将返回错误并中断后续处理。
func (s *ShortUrl) BatchCreate(in *ShortUrlBatchCreate) (*ShortUrlBatchCreateResult, error) {
path := "/openapi/short_url/batch/create"
if len(in.Urls) == 0 {
return nil, errors.New("url is empty")
}
if len(in.BatchName) == 0 {
in.BatchName = fmt.Sprintf("%s_%d", GenerateRandomLowerString(3), time.Now().Unix())
}
requestJson, err := StructToMap(in)
if err != nil {
return nil, err
}
req := Request{
Method: "POST",
Url: s.host + path,
Json: requestJson,
Headers: s.GetHeader(),
}
resp, err := req.Send()
if err != nil {
return nil, fmt.Errorf("请求失败err: %v", err)
}
var resData ShortUrlBatchCreateResult
err = CheckHttpRes(&resp, &resData)
if err != nil {
return nil, err
}
return &resData, nil
}
func (s *ShortUrl) BatchQuery(in *BatchQueryReq) (*QueryBatchWithUrlResp, error) {
path := "/openapi/short_url/batch/query"
if in.BatchNo == 0 && in.Id == 0 && len(in.BatchName) == 0 {
return nil, errors.New("batch_no,id,batch_name必传其中一个")
}
requestJson, err := StructToMap(in)
if err != nil {
return nil, err
}
req := Request{
Method: "POST",
Url: s.host + path,
Json: requestJson,
Headers: s.GetHeader(),
}
resp, err := req.Send()
if err != nil {
return nil, fmt.Errorf("请求失败err: %v", err)
}
var resData QueryBatchWithUrlResp
err = CheckHttpRes(&resp, &resData)
if err != nil {
return nil, err
}
return &resData, nil
}
func CheckHttpRes(resp *Response, data interface{}) (err error) {
var resBody ResCommon
if err = json.Unmarshal(resp.Content, &resBody); err != nil {
return
}
if resBody.Code != 0 {
return fmt.Errorf("请求失败err: %s", resBody.Message)
}
if data != nil {
dataByte, _ := json.Marshal(resBody.Data)
if err = json.Unmarshal(dataByte, data); err != nil {
return
}
}
return nil
}
func (s *ShortUrl) GetHeader() map[string]string {
return map[string]string{
"Content-Type": "application/json",
"Authorization": s.auth,
}
}

36
short_url_test.go Normal file
View File

@ -0,0 +1,36 @@
package l_short_utl_request
import (
"testing"
"time"
)
func TestBatchCreate(t *testing.T) {
in := &ShortUrlBatchCreate{
BatchName: "test_query_" + time.Now().Format("20060102150405"),
Urls: []string{
"https://www.baidu.com",
"https://github.com/trending",
"https://www.sojson.com/",
},
StartTimeStr: time.Now().Format(time.DateOnly),
EndTimeStr: time.Now().AddDate(0, 0, 2).Format(time.DateOnly),
WithBatchMap: true,
}
res, err := NewClient(WithAuth("eW10OjkxMmU3MWE2NGI3ZjkzNzlkYWRkNWYzMzRhM2M3MjM2OTdiNjJmOTJkMThkZDY3NTNjZWI1MzMwOWIwNjE2NDg=")).BatchCreate(in)
t.Log(res, err)
}
func TestBatchQuery(t *testing.T) {
// 查询批次
queryIn := &BatchQueryReq{
BatchNo: 4233953069,
}
queryRes, err := NewClient().BatchQuery(queryIn)
if err != nil {
t.Errorf("BatchQuery failed: %v", err)
}
t.Logf("Query Response: %+v", queryRes)
}