Files
cmautobuy/admin/service/profile.go
T

114 lines
3.8 KiB
Go
Raw Normal View History

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),
}
}