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