chore: 初始开源发布

iotlcm-upstream-chan:运营商上游通道适配服务(统一 /sim/* 协议)。

- 内联原私有依赖:yunze-public → internal/pub/(10 包 18 文件),
  yunze-pb/pb → internal/pb/(9 文件);Consul 注册改用官方
  gogf/gf/contrib/registry/consul/v2(API 兼容)
- Go module:yz-svr → git.5iot.com/iotlcm-public/iotlcm-upstream-chan
- 修复 mingqian 包测试:实现重构后 `adapter.operations` 字段已移除,
  测试断言按当前实现语义重写(不改产品行为)
- 凭据与内部标识脱敏:MySQL/Redis/Consul 凭据、私网与生产 IP、
  内网域名、真实手机号、注释里的真实 token/transid
- 移除内部资产:CI 触发器、生产通道种子 SQL、真实凭据测试资源文档
- Dockerfile 改用公共基础镜像,移除写死的 registry 凭据
- 补 Apache-2.0 LICENSE,重写 README,LF 行尾 + .gitattributes
This commit is contained in:
MiFaZhan
2026-10-22 15:42:29 +08:00
commit 96b1edff45
191 changed files with 30805 additions and 0 deletions
+156
View File
@@ -0,0 +1,156 @@
package dx
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// HTTPStatusError preserves the telecom response body for non-200 responses.
// RespCode is the raw carrier error code when the gateway supplies one.
type HTTPStatusError struct {
StatusCode int
Body string
RespCode string
}
func (e *HTTPStatusError) Error() string {
if e == nil {
return "telecom HTTP status error"
}
return fmt.Sprintf("http status %d: %s", e.StatusCode, e.Body)
}
// Config 网关鉴权与请求配置
type Config struct {
BaseURL string // 默认 https://cmp-api.ctwing.cn:20164
AppKey string
AppSecret string
Timeout time.Duration
UppercaseSign bool
}
// Client API 客户端
type Client struct {
baseURL string
appKey string
appSecret string
http *http.Client
uppercaseSign bool
}
// New 创建客户端
func NewClient(cfg Config) *Client {
base := cfg.BaseURL
if base == "" {
base = DefaultBaseURL
}
timeout := cfg.Timeout
if timeout <= 0 {
timeout = 10 * time.Second
}
return &Client{
baseURL: strings.TrimSuffix(base, "/"),
appKey: cfg.AppKey,
appSecret: cfg.AppSecret,
uppercaseSign: cfg.UppercaseSign,
http: &http.Client{
Timeout: timeout,
},
}
}
// DoGetRequest 通用GET请求(自动处理签名)
// endpoint: API路径,如 "/api/v1/openbill/queryTraffic"
// params: 查询参数,需调用方自行构建
func (c *Client) DoGetRequest(ctx context.Context, endpoint string, params url.Values) ([]byte, error) {
req, err := c.doGetRequest(ctx, endpoint, params)
if err != nil {
return nil, err
}
return c.doRequest(req)
}
func (c *Client) DoPostJSONRequest(ctx context.Context, endpoint string, body []byte) ([]byte, error) {
timestamp := GetTimestampGMT()
signStr, err := BuildSign("", string(body), c.appSecret, timestamp)
if c.uppercaseSign {
signStr, err = BuildSignV3("", string(body), c.appSecret, timestamp)
}
if err != nil {
return nil, err
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.baseURL+endpoint, strings.NewReader(string(body)))
if err != nil {
return nil, err
}
req.Header.Set("AppKey", c.appKey)
req.Header.Set("Sign", c.formatSign(signStr))
req.Header.Set("Timestamp", timestamp)
req.Header.Set("Content-Type", "application/json")
return c.doRequest(req)
}
// doGetRequest 封装通用的 GET 请求构建逻辑
func (c *Client) doGetRequest(ctx context.Context, endpoint string, params url.Values) (*http.Request, error) {
queryString := params.Encode()
timestamp := GetTimestampGMT()
signStr, err := BuildSign(queryString, "", c.appSecret, timestamp)
if c.uppercaseSign {
signStr, err = BuildSignV3(queryString, "", c.appSecret, timestamp)
}
if err != nil {
return nil, err
}
reqURL := c.baseURL + endpoint + "?" + queryString
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
if err != nil {
return nil, err
}
req.Header.Set("AppKey", c.appKey)
req.Header.Set("Sign", c.formatSign(signStr))
req.Header.Set("Timestamp", timestamp)
return req, nil
}
func (c *Client) formatSign(sign string) string {
if c.uppercaseSign {
return strings.ToUpper(sign)
}
return sign
}
// doRequest 执行HTTP请求
func (c *Client) doRequest(req *http.Request) ([]byte, error) {
resp, err := c.http.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
var errorBody struct {
RespCode string `json:"respCode"`
}
_ = json.Unmarshal(body, &errorBody)
if c.uppercaseSign && strings.TrimSpace(errorBody.RespCode) == "" {
errorBody.RespCode = resp.Header.Get("gw-errorCode")
}
return nil, &HTTPStatusError{
StatusCode: resp.StatusCode,
Body: string(body),
RespCode: strings.TrimSpace(errorBody.RespCode),
}
}
return body, nil
}
+69
View File
@@ -0,0 +1,69 @@
package dx
import (
"crypto/md5"
"encoding/hex"
"sort"
"strings"
"time"
)
const (
// DefaultBaseURL 电信API基础地址
DefaultBaseURL = "https://cmp-api.ctwing.cn:20164"
)
// GetTimestampGMT 返回 GMT 时区的 yyyyMMddHHmmss 格式时间戳
func GetTimestampGMT() string {
return time.Now().UTC().Format("20060102150405")
}
// OrderParams 将 URL params 按 key 字典序排序
func OrderParams(params string) (string, error) {
if params == "" {
return "", nil
}
kvs := strings.Split(params, "&")
m := make(map[string]string, len(kvs))
for _, kv := range kvs {
idx := strings.Index(kv, "=")
if idx < 0 {
continue
}
m[kv[:idx]] = kv[idx+1:]
}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
var parts []string
for _, k := range keys {
parts = append(parts, k+"="+m[k])
}
return strings.Join(parts, "&"), nil
}
// SignMD5 对 reqStr 做 MD5 加密,返回小写十六进制字符串
func SignMD5(reqStr string) string {
h := md5.Sum([]byte(reqStr))
return strings.ToLower(hex.EncodeToString(h[:]))
}
// BuildSign 构造签名:params + body + secretKey + timestamp → MD5 → hex
func BuildSign(params, body, secretKey, timestamp string) (string, error) {
ordered, err := OrderParams(params)
if err != nil {
return "", err
}
reqStr := ordered + body + secretKey + timestamp
md5Str := SignMD5(reqStr)
return hex.EncodeToString([]byte(md5Str)), nil
}
// BuildSignV3 follows the V3 Java sample: encode the lowercase MD5 digest
// string as hex a second time, then uppercase that outer encoding.
func BuildSignV3(params, body, secretKey, timestamp string) (string, error) {
sign, err := BuildSign(params, body, secretKey, timestamp)
return strings.ToUpper(sign), err
}
@@ -0,0 +1,15 @@
package dx
import "testing"
func TestV3SignatureKeepsLegacyEncodingAndUsesUppercase(t *testing.T) {
// MD5("abc") is 900150983cd24fb0d6963f7d28e17f72.
legacy, err := BuildSign("", "abc", "", "")
if err != nil || legacy != "3930303135303938336364323466623064363936336637643238653137663732" {
t.Fatalf("legacy signature: %s, %v", legacy, err)
}
v3, err := BuildSignV3("", "abc", "", "")
if err != nil || v3 != "3930303135303938336364323466623064363936336637643238653137663732" {
t.Fatalf("v3 signature: %s, %v", v3, err)
}
}