Compare commits

..

9 Commits
v1.4.0 ... main

Author SHA1 Message Date
zhongqiang
f7a1a30203 新增物流接口 2025-06-23 16:59:47 +08:00
zhongqiang
9eecda8c5f 新增物流接口 2025-06-23 16:00:54 +08:00
zhongqiang
12db943012 新增物流接口 2025-06-20 18:03:39 +08:00
yuguojian
3676395e9f Merge remote-tracking branch 'origin/feature/对接新供应链-唯品尚' 2025-06-20 15:53:30 +08:00
zhongqiang
2fd998b52d 对接新供应链-唯品尚 2025-06-20 15:51:48 +08:00
lzh
2e8f0cb3f2 sms_tool 2025-06-20 14:49:45 +08:00
yuguojian
992f39109d 优化ip缓存 2025-06-18 18:06:45 +08:00
yuguojian
db27554374 Merge branch 'feature/sms_tool' 2025-06-18 17:41:21 +08:00
lzh
e2d9685db3 sms_tool 2025-06-18 17:37:21 +08:00
9 changed files with 352 additions and 45 deletions

View File

@ -0,0 +1,161 @@
package express_tool
import (
"context"
"encoding/json"
"fmt"
"github.com/pkg/errors"
"io"
"log"
"net/http"
"net/url"
"time"
)
type AliCloudExpressClient struct {
AppCode string
Host string
cache ICacheAdapter
}
func NewAliCloudExpressClient(host, appCode string) *AliCloudExpressClient {
return &AliCloudExpressClient{
AppCode: appCode,
Host: host,
}
}
func (a *AliCloudExpressClient) GetLogisticsInfo(mobile, number string) (res *ExpressRes, err error) {
if mobile == "" || number == "" {
return nil, errors.New("请输入手机号和物流单号")
}
// 设置参数
params := url.Values{}
params.Add("mobile", mobile)
params.Add("number", number)
// 拼接URL
var fullURL string
fullURL, err = url.JoinPath(a.Host)
if err != nil {
return nil, errors.Wrapf(err, "查询物流信息失败, 拼接路径错误! 手机号:%s, 物流单号:%s", mobile, number)
}
// 拼接参数
fullURL = fmt.Sprintf("%s?%s", fullURL, params.Encode())
// 创建HTTP客户端
client := &http.Client{}
// 创建请求
var req *http.Request
req, err = http.NewRequest(http.MethodGet, fullURL, nil)
if err != nil {
return nil, errors.Wrapf(err, "查询物流信息失败, 创建请求失败! 手机号:%s, 物流单号:%s", mobile, number)
}
// 设置请求头
req.Header.Add("Authorization", "APPCODE "+a.AppCode)
// 发送请求
client.Timeout = 2 * time.Second
var resp *http.Response
resp, err = client.Do(req)
if err != nil {
return nil, errors.Wrapf(err, "查询物流信息失败, 发送请求失败! 手机号:%s, 物流单号:%s", mobile, number)
}
defer func(Body io.ReadCloser) {
err = Body.Close()
if err != nil {
log.Printf("查询物流信息失败, 关闭响应体失败! 手机号:%s, 物流单号:%s , %+v\n", mobile, number, err)
}
}(resp.Body)
// 读取响应体
var body []byte
body, err = io.ReadAll(resp.Body)
if err != nil {
return nil, errors.Wrapf(err, "查询物流信息失败, 读取响应体失败! 手机号:%s, 物流单号:%s", mobile, number)
}
//log.Printf("查询物流信息成功! %s\n", string(body))
// 解析JSON响应
var expressRes ExpressRes
err = json.Unmarshal(body, &expressRes)
if err != nil {
return nil, errors.Wrapf(err, "查询物流信息失败, 解析JSON响应失败, %s! 手机号:%s, 物流单号:%s", string(body), mobile, number)
}
if expressRes.Code != 0 {
return &expressRes, errors.Wrapf(err, "查询物流信息失败! expressRes:%+v 手机号:%s, 物流单号:%s", expressRes, mobile, number)
}
if expressRes.Data == nil {
return &expressRes, errors.Wrapf(err, "查询物流信息失败,没有查询到物流信息! expressRes:%+v 手机号:%s, 物流单号:%s", expressRes, mobile, number)
}
return &expressRes, nil
}
func (a *AliCloudExpressClient) Set(cache ICacheAdapter) {
a.cache = cache
}
func (a *AliCloudExpressClient) GetLogisticsInfoFormCache(ctx context.Context, mobile, prefix, number string, opt ...time.Duration) (res *ExpressRes, err error) {
if a.cache != nil {
res, err = a.cache.Get(ctx, a.numberKey(prefix, number))
if err != nil {
return nil, errors.Wrapf(err, "获取缓存失败, number:%s", number)
}
if res != nil {
return res, nil
}
}
res, err = a.GetLogisticsInfo(mobile, number)
if err != nil {
return nil, errors.Wrapf(err, "获取物流信息失败, number:%s", number)
}
var infoJson []byte
infoJson, err = json.Marshal(res)
if err != nil {
return nil, errors.Wrapf(err, "无法将物流信息转换为JSON, number:%s", number)
}
if len(opt) > 0 {
err = a.cache.Set(ctx, a.numberKey(prefix, number), string(infoJson), opt[0])
if err != nil {
return nil, errors.Wrapf(err, "缓存物流信息失败, number:%s", number)
}
}
return
}
func (a *AliCloudExpressClient) DeleteLogisticsInfoCache(ctx context.Context, prefix, number string) (err error) {
if a.cache == nil {
return errors.New("缓存不能为空")
}
err = a.cache.Del(ctx, a.numberKey(prefix, number))
return err
}
// ipKey 生成Redis key
func (a *AliCloudExpressClient) numberKey(prefix, number string) string {
return fmt.Sprintf("%s:number:%s", prefix, number)
}
type ExpressRes struct {
Code int `json:"code"`
Desc string `json:"desc"`
Data *Data `json:"data"`
}
type Data struct {
State int `json:"state" dc:"物流状态【1在途中2派件中3已签收4派送失败5揽收6退回7转单8疑难9退签10待清关11清关中12已清关13清关异常】"`
Name string `json:"name" dc:"物流名"`
Com string `json:"com"`
Number string `json:"number" dc:"单号"`
Logo string `json:"logo" dc:"图标地址"`
List []*List `json:"list"`
}
type List struct {
Time string `json:"time"`
Status string `json:"status"`
}

View File

@ -0,0 +1,130 @@
package express_tool
import (
"context"
"encoding/json"
"errors"
"fmt"
"github.com/redis/go-redis/v9"
"log"
"testing"
"time"
)
func TestAliCloudExpressClient_GetLogisticsInfo(t *testing.T) {
type fields struct {
AppCode string
Host string
}
type args struct {
mobile string
number string
}
tests := []struct {
name string
fields fields
args args
wantRes *ExpressRes
wantErr bool
}{
{
name: "test1",
fields: fields{
AppCode: "",
Host: "",
},
args: args{
mobile: "",
number: "",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a := &AliCloudExpressClient{
AppCode: tt.fields.AppCode,
Host: tt.fields.Host,
}
gotRes, err := a.GetLogisticsInfo(tt.args.mobile, tt.args.number)
log.Println(gotRes, err)
})
}
}
func TestAliCloudExpressClient_GetLogisticsInfoFormCache(t *testing.T) {
rdb := redis.NewClient(&redis.Options{
Addr: "",
Password: "",
DB: 1,
})
// 创建缓存实例
cache := NewRedisCache(rdb)
a := &AliCloudExpressClient{
AppCode: "",
Host: "",
cache: cache,
}
gotRes, err := a.GetLogisticsInfoFormCache(context.Background(), "", "", "", time.Minute)
log.Println(gotRes, err)
}
func TestAliCloudExpressClient_DeleteLogisticsInfoCache(t *testing.T) {
rdb := redis.NewClient(&redis.Options{
Addr: "",
Password: "",
DB: 1,
})
// 创建缓存实例
cache := NewRedisCache(rdb)
a := &AliCloudExpressClient{
AppCode: "",
Host: "",
cache: cache,
}
err := a.DeleteLogisticsInfoCache(context.Background(), "", "")
log.Println(err)
}
type RedisCache struct {
client *redis.Client
}
func (r *RedisCache) Del(ctx context.Context, number string) error {
return r.client.Del(ctx, number).Err()
}
func NewRedisCache(client *redis.Client) *RedisCache {
return &RedisCache{client: client}
}
func (r *RedisCache) Set(ctx context.Context, number string, res string, ttl time.Duration) error {
if number == "" {
return errors.New("number不能为空")
}
return r.client.Set(ctx, number, res, ttl).Err()
}
// Get 从Redis获取IP信息
func (r *RedisCache) Get(ctx context.Context, number string) (*ExpressRes, error) {
if number == "" {
return nil, errors.New("number不能为空")
}
data, err := r.client.Get(ctx, number).Bytes()
if err != nil {
if errors.Is(err, redis.Nil) {
return nil, nil // 键不存在返回nil而不是错误
}
return nil, fmt.Errorf("无法获取物流信息: %w", err)
}
var info ExpressRes
if err = json.Unmarshal(data, &info); err != nil {
return nil, fmt.Errorf("无法解析物流信息: %w", err)
}
return &info, nil
}

View File

@ -0,0 +1,12 @@
package express_tool
import (
"context"
"time"
)
type ICacheAdapter interface {
Set(ctx context.Context, key string, res string, ttl time.Duration) error
Get(ctx context.Context, key string) (*ExpressRes, error)
Del(ctx context.Context, key string) error
}

View File

@ -19,11 +19,10 @@ type HuaChenIpClient struct {
cache ICacheAdapter cache ICacheAdapter
} }
func NewHuaChenIpClient(appCode string, cache ICacheAdapter) *HuaChenIpClient { func NewHuaChenIpClient(appCode string) *HuaChenIpClient {
return &HuaChenIpClient{ return &HuaChenIpClient{
AppCode: appCode, AppCode: appCode,
Host: "https://c2ba.api.huachen.cn", Host: "https://c2ba.api.huachen.cn",
cache: cache,
} }
} }
@ -90,13 +89,19 @@ func (h *HuaChenIpClient) GetIpInfo(ip string) (res *ApiResult, err error) {
return &apiResult, nil return &apiResult, nil
} }
func (h *HuaChenIpClient) GetIpInfoFormCache(ctx context.Context, ip string, ttl time.Duration) (res *ApiResult, err error) { func (h *HuaChenIpClient) Set(cache ICacheAdapter) {
getCache, err := h.cache.Get(ctx, h.ipKey(ip)) h.cache = cache
if err != nil { }
return nil, errors.Wrapf(err, "获取缓存失败ip:%s", ip)
} func (h *HuaChenIpClient) GetIpInfoFormCache(ctx context.Context, ip string, opt ...time.Duration) (res *ApiResult, err error) {
if getCache != nil { if h.cache != nil {
return getCache, nil res, err = h.cache.Get(ctx, h.ipKey(ip))
if err != nil {
return nil, errors.Wrapf(err, "获取缓存失败ip:%s", ip)
}
if res != nil {
return res, nil
}
} }
res, err = h.GetIpInfo(ip) res, err = h.GetIpInfo(ip)
if err != nil { if err != nil {
@ -109,9 +114,11 @@ func (h *HuaChenIpClient) GetIpInfoFormCache(ctx context.Context, ip string, ttl
return nil, errors.Wrapf(err, "无法将IP信息转换为JSONip:%s", ip) return nil, errors.Wrapf(err, "无法将IP信息转换为JSONip:%s", ip)
} }
err = h.cache.Set(ctx, h.ipKey(ip), string(infoJson), ttl) if len(opt) == 0 {
if err != nil { err = h.cache.Set(ctx, h.ipKey(ip), string(infoJson), opt[0])
return nil, errors.Wrapf(err, "缓存ip:%s信息失败", ip) if err != nil {
return nil, errors.Wrapf(err, "缓存ip:%s信息失败", ip)
}
} }
return return
} }

View File

@ -9,4 +9,5 @@ type ICacheAdapter interface {
Get(ctx context.Context, key string) (value interface{}, err error) Get(ctx context.Context, key string) (value interface{}, err error)
Set(ctx context.Context, key string, value interface{}, expire time.Duration) (err error) Set(ctx context.Context, key string, value interface{}, expire time.Duration) (err error)
Del(ctx context.Context, key string) (err error) Del(ctx context.Context, key string) (err error)
SetNX(ctx context.Context, key string, value interface{}, expire time.Duration) (ok bool, err error)
} }

View File

@ -8,7 +8,6 @@ import (
dysmsapi20170525 "github.com/alibabacloud-go/dysmsapi-20170525/v5/client" dysmsapi20170525 "github.com/alibabacloud-go/dysmsapi-20170525/v5/client"
util "github.com/alibabacloud-go/tea-utils/v2/service" util "github.com/alibabacloud-go/tea-utils/v2/service"
"github.com/alibabacloud-go/tea/tea" "github.com/alibabacloud-go/tea/tea"
"github.com/redis/go-redis/v9"
"time" "time"
) )
@ -71,7 +70,15 @@ func (c *SmsClient) GetCode(ctx context.Context, key string) (code string, err e
if err != nil { if err != nil {
return "", err return "", err
} }
return value.(string), nil if value == nil {
return "", errors.New("验证码不存在,请重新发送")
}
switch value.(type) {
case string:
return value.(string), nil
default:
return "", errors.New("验证码类型错误,请联系管理员")
}
} }
// SaveCode 保存验证码 // SaveCode 保存验证码
@ -81,10 +88,7 @@ func (c *SmsClient) SaveCode(ctx context.Context, key string, code string, expir
} }
//保存code //保存code
err = c.Cache.Set(ctx, key, code, expire) err = c.Cache.Set(ctx, key, code, expire)
if err != nil { return err
return err
}
return nil
} }
// DeleteCode 删除验证码 // DeleteCode 删除验证码
@ -102,34 +106,18 @@ func (c *SmsClient) SaveCodeWithFrequency(ctx context.Context, key string, code
return errors.New("缓存不能为空") return errors.New("缓存不能为空")
} }
frequencyKey := fmt.Sprintf(CodeFrequencyKey, key) frequencyKey := fmt.Sprintf(CodeFrequencyKey, key)
if frequency != 0 { if frequency <= 0 {
// 判断验证码是否频繁 return errors.New("频率不能小于0")
frequencyValue, err := c.Cache.Get(ctx, frequencyKey)
if err != nil {
if errors.Is(err, redis.Nil) {
// 缓存中没有数据
fmt.Printf("缓存中没有数据")
} else {
return err
}
}
if frequencyValue != nil && frequencyValue.(string) == CodeFrequencyValue {
return errors.New("code发送频繁请稍后再试")
}
} }
//保存code ok, err := c.Cache.SetNX(ctx, frequencyKey, CodeFrequencyValue, frequency)
err = c.SaveCode(ctx, key, code, expire)
if err != nil { if err != nil {
return err return err
} }
if frequency != 0 { if !ok {
// 保存验证码发送频率 return errors.New("code发送频繁请稍后再试")
err = c.Cache.Set(ctx, frequencyKey, CodeFrequencyValue, frequency)
if err != nil {
return err
}
} }
return nil err = c.SaveCode(ctx, key, code, expire)
return err
} }
// VerifyCode 校验验证码 // VerifyCode 校验验证码

View File

@ -51,6 +51,14 @@ func (r *RedisCacheAdapter) Del(ctx context.Context, key string) error {
return r.client.Del(ctx, key).Err() return r.client.Del(ctx, key).Err()
} }
func (r *RedisCacheAdapter) SetNX(ctx context.Context, key string, value interface{}, expire time.Duration) (ok bool, err error) {
data, err := json.Marshal(value)
if err != nil {
return false, err
}
return r.client.SetNX(ctx, key, data, expire).Result()
}
var ( var (
//SMS //SMS
accessKeyId = os.Getenv("SMS_ALIBABA_CLOUD_ACCESS_KEY_ID") accessKeyId = os.Getenv("SMS_ALIBABA_CLOUD_ACCESS_KEY_ID")

View File

@ -33,7 +33,7 @@ func (w *WeiPinShangClient) GetManyPostage(getManyPostageReq *GetManyPostageReq)
fmt.Println("getManyPostageReq", getManyPostageReq) fmt.Println("getManyPostageReq", getManyPostageReq)
paramMap := make(map[string]any) paramMap := make(map[string]any)
paramMap["goodInfo"] = getManyPostageReq.GoodsInfo paramMap["goodsInfo"] = getManyPostageReq.GoodsInfo
paramMap["address"] = getManyPostageReq.Address paramMap["address"] = getManyPostageReq.Address
paramMap["province"] = getManyPostageReq.Province paramMap["province"] = getManyPostageReq.Province
paramMap["county"] = getManyPostageReq.County paramMap["county"] = getManyPostageReq.County

View File

@ -29,7 +29,7 @@ func TestWeiPinShangClient_GetManyPostage(t *testing.T) {
Key: "f654ea5bde7635c3f46191191e5c4c8e", Key: "f654ea5bde7635c3f46191191e5c4c8e",
}, },
args: GetManyPostageReq{ args: GetManyPostageReq{
GoodsInfo: "[{\"goodsId\":\"WPS592_00019\",\"goodSpecId\":\"WPS592_1105165115160944\",\"num\":1},{\"goodsId\":\"WPS505_00007\",\"goodSpecId\":\"WPS505_1007111249857536\",\"num\":1}]", GoodsInfo: "[{\"goodsId\":\"WPS427_adf0008\",\"goodSpecId\":\"WPS427_0715110641454716\",\"num\":1}]",
Province: "广东省", Province: "广东省",
Address: "奥园", Address: "奥园",
City: "广州市", City: "广州市",
@ -80,14 +80,14 @@ func TestWeiPinShangClient_PreOrder(t *testing.T) {
Key: "f654ea5bde7635c3f46191191e5c4c8e", Key: "f654ea5bde7635c3f46191191e5c4c8e",
}, },
args: PreOrderReq{ args: PreOrderReq{
GoodsInfo: "[{\"goodsId\":\"WPS2_1231155626421463\",\"goodSpecId\":\"WPS2_12311556265677476\",\"num\":1}]", GoodsInfo: "[{\"goodsId\":\"WPS427_adf0008\",\"goodSpecId\":\"WPS427_0715110641454716\",\"num\":1}]",
Province: "广东省", Province: "广东省",
Address: "奥园", Address: "奥园",
City: "广州市", City: "广州市",
Area: "番禺区", Area: "番禺区",
ConsigneePhone: "15375390426", ConsigneePhone: "15375390426",
ConsigneeContacts: "张三", ConsigneeContacts: "张三",
LockCode: "L100000004", LockCode: "L100000005",
Source: "AILEHUI", Source: "AILEHUI",
}, },
}, },