114 lines
3.8 KiB
Go
114 lines
3.8 KiB
Go
package service
|
||||
|
|
|
|||
|
|
import (
|
|||
|
|
"encoding/json"
|
|||
|
|
"errors"
|
|||
|
|
"fmt"
|
|||
|
|
"strings"
|
|||
|
|
"unicode/utf8"
|
|||
|
|
|
|||
|
|
"cmautobuy/admin/model"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// ErrInvalidProfile 表示客户端上报的身份/能力字段不合法 -> 422。
|
|||
|
|
var ErrInvalidProfile = errors.New("客户端身份或能力字段无效")
|
|||
|
|
|
|||
|
|
// 名称长度上限,和 Client 设置页那个输入框的 setMaxLength(50) 对齐。
|
|||
|
|
const maxClientNameRunes = 50
|
|||
|
|
|
|||
|
|
// ClientProfileRequest 是客户端上报的身份、设备和能力。
|
|||
|
|
//
|
|||
|
|
// **登记接口和领取接口共用这一个结构**,校验也共用 Validate()。
|
|||
|
|
// 不要为两个接口各写一份——名称规则本来就不同了,
|
|||
|
|
// 再让结构和校验也分开维护,迟早漂移成两套行为。
|
|||
|
|
type ClientProfileRequest struct {
|
|||
|
|
Client struct {
|
|||
|
|
Name string `json:"name"`
|
|||
|
|
} `json:"client"`
|
|||
|
|
|
|||
|
|
SupportedTypes []string `json:"supported_types"`
|
|||
|
|
|
|||
|
|
Device struct {
|
|||
|
|
Address string `json:"address"`
|
|||
|
|
Platform string `json:"platform"`
|
|||
|
|
PddPackage string `json:"pdd_package"`
|
|||
|
|
} `json:"device"`
|
|||
|
|
|
|||
|
|
Capabilities struct {
|
|||
|
|
PurchaseMode string `json:"purchase_mode"`
|
|||
|
|
SchemaVersions []int `json:"schema_versions"`
|
|||
|
|
} `json:"capabilities"`
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// Validate 检查上报的字段是否合法。
|
|||
|
|
//
|
|||
|
|
// 规则来自 docs/client/04-admin-api-contract.md §4.1。
|
|||
|
|
// 错误信息要说清**哪个字段、错在哪**,不要只说"参数无效"——
|
|||
|
|
// 客户端开发者拿到这条信息就得能定位问题。
|
|||
|
|
func (p ClientProfileRequest) Validate() error {
|
|||
|
|
// 名称:可以为空,但不能超长。按字符数算,不是字节数——
|
|||
|
|
// 中文一个字三字节,按字节算会把 17 个汉字就判成超长。
|
|||
|
|
if n := utf8.RuneCountInString(p.Client.Name); n > maxClientNameRunes {
|
|||
|
|
return fmt.Errorf("%w: client.name 最多 %d 个字,收到 %d 个",
|
|||
|
|
ErrInvalidProfile, maxClientNameRunes, n)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 任务类型:必须非空,且只能是这两种
|
|||
|
|
if len(p.SupportedTypes) == 0 {
|
|||
|
|
return fmt.Errorf("%w: supported_types 不能为空", ErrInvalidProfile)
|
|||
|
|
}
|
|||
|
|
for _, t := range p.SupportedTypes {
|
|||
|
|
if t != string(model.TaskCollect) && t != string(model.TaskPurchase) {
|
|||
|
|
return fmt.Errorf("%w: supported_types 只允许 collect / purchase,收到 %q",
|
|||
|
|
ErrInvalidProfile, t)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 设备平台:可以不填,填了当前只支持 android
|
|||
|
|
if plat := p.Device.Platform; plat != "" && plat != "android" {
|
|||
|
|
return fmt.Errorf("%w: device.platform 当前只支持 android,收到 %q",
|
|||
|
|
ErrInvalidProfile, plat)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 执行模式:必填。
|
|||
|
|
// 这个字段决定 Admin 敢不敢把需要真实下单的任务发给它,
|
|||
|
|
// 不能靠猜——宁可让客户端明确声明。
|
|||
|
|
switch p.Capabilities.PurchaseMode {
|
|||
|
|
case "dry_run", "live":
|
|||
|
|
case "":
|
|||
|
|
return fmt.Errorf("%w: capabilities.purchase_mode 必填(dry_run 或 live)",
|
|||
|
|
ErrInvalidProfile)
|
|||
|
|
default:
|
|||
|
|
return fmt.Errorf("%w: capabilities.purchase_mode 只允许 dry_run / live,收到 %q",
|
|||
|
|
ErrInvalidProfile, p.Capabilities.PurchaseMode)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 结构版本:必须非空且都是正整数
|
|||
|
|
if len(p.Capabilities.SchemaVersions) == 0 {
|
|||
|
|
return fmt.Errorf("%w: capabilities.schema_versions 不能为空", ErrInvalidProfile)
|
|||
|
|
}
|
|||
|
|
for _, v := range p.Capabilities.SchemaVersions {
|
|||
|
|
if v <= 0 {
|
|||
|
|
return fmt.Errorf("%w: capabilities.schema_versions 只能是正整数,收到 %d",
|
|||
|
|
ErrInvalidProfile, v)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ToClient 把上报内容转成要落库的 Client。
|
|||
|
|
//
|
|||
|
|
// capabilities 原样序列化存起来——将来排查问题时,
|
|||
|
|
// 能看到这台客户端当时到底声明了什么。
|
|||
|
|
func (p ClientProfileRequest) ToClient(clientID string) model.Client {
|
|||
|
|
caps, _ := json.Marshal(p.Capabilities)
|
|||
|
|
return model.Client{
|
|||
|
|
ClientID: clientID,
|
|||
|
|
Name: strings.TrimSpace(p.Client.Name),
|
|||
|
|
DeviceAddress: p.Device.Address,
|
|||
|
|
Platform: p.Device.Platform,
|
|||
|
|
PddPackage: p.Device.PddPackage,
|
|||
|
|
Capabilities: string(caps),
|
|||
|
|
}
|
|||
|
|
}
|