|
|
@@ -0,0 +1,420 @@
|
|
|
+package service
|
|
|
+
|
|
|
+import (
|
|
|
+ "bytes"
|
|
|
+ "crypto/sha256"
|
|
|
+ "crypto/tls"
|
|
|
+ "encoding/hex"
|
|
|
+ "encoding/json"
|
|
|
+ "errors"
|
|
|
+ "fmt"
|
|
|
+ "io"
|
|
|
+ "net"
|
|
|
+ "net/http"
|
|
|
+ "net/url"
|
|
|
+ "strconv"
|
|
|
+ "strings"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ "github.com/gofrs/uuid/v5"
|
|
|
+ "gorm.io/gorm"
|
|
|
+ "wails-app/internal/dao"
|
|
|
+ "wails-app/internal/global"
|
|
|
+ "wails-app/internal/modules/deviceprovisioning/model/request"
|
|
|
+ responseModel "wails-app/internal/modules/deviceprovisioning/model/response"
|
|
|
+ "wails-app/internal/modules/deviceprovisioning/repository"
|
|
|
+)
|
|
|
+
|
|
|
+const (
|
|
|
+ defaultConfigPort = 8443
|
|
|
+ identityPath = "/api/v1/identity"
|
|
|
+ provisionPath = "/api/v1/provision"
|
|
|
+)
|
|
|
+
|
|
|
+// Service 实现手工 IP 接入流程。所有设备身份都以 HTTPS 证书和永久 ID 为准。
|
|
|
+type Service struct{ repo *repository.Repository }
|
|
|
+
|
|
|
+func New() *Service { return &Service{repo: repository.New()} }
|
|
|
+
|
|
|
+// findProvisionReader finds the row to update for a provisioning request.
|
|
|
+// Unscoped is intentional: device_code remains unique after a soft delete.
|
|
|
+func findProvisionReader(db *gorm.DB, deviceID, deviceCode string) (dao.UHFReader, bool, error) {
|
|
|
+ var existing dao.UHFReader
|
|
|
+ var duplicate dao.UHFReader
|
|
|
+ duplicateResult := db.Unscoped().Where("device_code = ?", deviceCode).Order("id DESC").First(&duplicate)
|
|
|
+ if duplicateResult.Error != nil && !errors.Is(duplicateResult.Error, gorm.ErrRecordNotFound) {
|
|
|
+ return existing, false, duplicateResult.Error
|
|
|
+ }
|
|
|
+ if duplicateResult.Error == nil {
|
|
|
+ if duplicate.DeviceID != deviceID {
|
|
|
+ return existing, false, errors.New("设备编码已被其他设备使用")
|
|
|
+ }
|
|
|
+ return duplicate, true, nil
|
|
|
+ }
|
|
|
+
|
|
|
+ result := db.Unscoped().Where("device_id = ?", deviceID).Order("id DESC").First(&existing)
|
|
|
+ if result.Error != nil && !errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
|
|
+ return existing, false, result.Error
|
|
|
+ }
|
|
|
+ return existing, result.Error == nil, nil
|
|
|
+}
|
|
|
+
|
|
|
+type deviceIdentity struct {
|
|
|
+ DeviceID string `json:"device_id"`
|
|
|
+ DeviceCode string `json:"device_code"`
|
|
|
+ DeviceType string `json:"device_type"`
|
|
|
+ DeviceModel string `json:"device_model"`
|
|
|
+ FirmwareVersion string `json:"firmware_version"`
|
|
|
+ CertificateFingerprint string `json:"certificate_fingerprint"`
|
|
|
+ PublicKeyFingerprint string `json:"public_key_fingerprint"`
|
|
|
+ PairingRequired bool `json:"pairing_required"`
|
|
|
+}
|
|
|
+
|
|
|
+func normalizeFingerprint(value string) string {
|
|
|
+ value = strings.ToLower(strings.TrimSpace(value))
|
|
|
+ value = strings.TrimPrefix(value, "sha256:")
|
|
|
+ value = strings.ReplaceAll(value, ":", "")
|
|
|
+ value = strings.ReplaceAll(value, "-", "")
|
|
|
+ return value
|
|
|
+}
|
|
|
+
|
|
|
+// ValidateAddress 只接受字面单播 IP,禁止回环、未指定、广播、组播和主机名。
|
|
|
+func ValidateAddress(rawIP string, port int) (net.IP, error) {
|
|
|
+ value := strings.TrimSpace(rawIP)
|
|
|
+ if value == "" || strings.EqualFold(value, "localhost") {
|
|
|
+ return nil, errors.New("设备地址必须是合法 IP,不能使用 localhost")
|
|
|
+ }
|
|
|
+ if net.ParseIP(value) == nil {
|
|
|
+ return nil, errors.New("设备地址必须是字面 IP,不能使用主机名")
|
|
|
+ }
|
|
|
+ ip := net.ParseIP(value)
|
|
|
+ if ip.IsLoopback() || ip.IsUnspecified() || ip.IsMulticast() {
|
|
|
+ return nil, errors.New("禁止使用回环、未指定或组播地址")
|
|
|
+ }
|
|
|
+ if port < 1 || port > 65535 {
|
|
|
+ return nil, errors.New("HTTPS 端口必须是 1-65535 的整数")
|
|
|
+ }
|
|
|
+ if ip.To4() != nil {
|
|
|
+ v4 := ip.To4()
|
|
|
+ if v4[0] == 255 && v4[1] == 255 && v4[2] == 255 && v4[3] == 255 {
|
|
|
+ return nil, errors.New("禁止使用广播地址")
|
|
|
+ }
|
|
|
+ if isDirectedBroadcast(v4) {
|
|
|
+ return nil, errors.New("禁止使用当前网卡的定向广播地址")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return ip, nil
|
|
|
+}
|
|
|
+
|
|
|
+func isDirectedBroadcast(ip net.IP) bool {
|
|
|
+ interfaces, _ := net.Interfaces()
|
|
|
+ for _, iface := range interfaces {
|
|
|
+ addrs, _ := iface.Addrs()
|
|
|
+ for _, addr := range addrs {
|
|
|
+ var ipnet *net.IPNet
|
|
|
+ switch value := addr.(type) {
|
|
|
+ case *net.IPNet:
|
|
|
+ ipnet = value
|
|
|
+ case *net.IPAddr:
|
|
|
+ ipnet = &net.IPNet{IP: value.IP, Mask: net.CIDRMask(32, 32)}
|
|
|
+ }
|
|
|
+ if ipnet == nil || ipnet.IP.To4() == nil || !ipnet.Contains(ip) {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+ network := ipnet.IP.To4()
|
|
|
+ mask := ipnet.Mask
|
|
|
+ return isDirectedBroadcastForNetwork(ip, network, mask)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false
|
|
|
+}
|
|
|
+
|
|
|
+func isDirectedBroadcastForNetwork(ip, network net.IP, mask net.IPMask) bool {
|
|
|
+ input := ip.To4()
|
|
|
+ network = network.To4()
|
|
|
+ if input == nil || network == nil || len(mask) != net.IPv4len {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ broadcast := make(net.IP, net.IPv4len)
|
|
|
+ for i := 0; i < net.IPv4len; i++ {
|
|
|
+ broadcast[i] = network[i] | ^mask[i]
|
|
|
+ }
|
|
|
+ return broadcast.Equal(input)
|
|
|
+}
|
|
|
+
|
|
|
+func (s *Service) httpClient(_ net.IP, fingerprint string) *http.Client {
|
|
|
+ transport := &http.Transport{TLSClientConfig: &tls.Config{
|
|
|
+ MinVersion: tls.VersionTLS12,
|
|
|
+ InsecureSkipVerify: true, // 设备出厂可使用自签名证书;VerifyConnection 负责固定指纹。
|
|
|
+ VerifyConnection: func(state tls.ConnectionState) error {
|
|
|
+ if len(state.PeerCertificates) == 0 {
|
|
|
+ return errors.New("设备未提供 TLS 证书")
|
|
|
+ }
|
|
|
+ cert := state.PeerCertificates[0]
|
|
|
+ got := sha256.Sum256(cert.Raw)
|
|
|
+ actual := hex.EncodeToString(got[:])
|
|
|
+ if fingerprint != "" && normalizeFingerprint(fingerprint) != actual {
|
|
|
+ return fmt.Errorf("证书指纹不匹配")
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+ },
|
|
|
+ }}
|
|
|
+ transport.DialContext = (&net.Dialer{Timeout: 5 * time.Second}).DialContext
|
|
|
+ return &http.Client{Transport: transport, Timeout: 10 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
|
|
|
+ return errors.New("禁止 HTTPS 重定向到其他主机")
|
|
|
+ }}
|
|
|
+}
|
|
|
+
|
|
|
+func endpoint(ip net.IP, port int, path string) string {
|
|
|
+ return (&url.URL{Scheme: "https", Host: net.JoinHostPort(ip.String(), strconv.Itoa(port)), Path: path}).String()
|
|
|
+}
|
|
|
+
|
|
|
+func decodeResponse(body []byte, target interface{}) error {
|
|
|
+ if len(body) > 64*1024 {
|
|
|
+ return errors.New("设备响应过大")
|
|
|
+ }
|
|
|
+ if err := json.Unmarshal(body, target); err != nil {
|
|
|
+ return fmt.Errorf("设备响应不是合法 JSON: %w", err)
|
|
|
+ }
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+// ReadIdentity 连接设备并保存 10 分钟有效的身份记录。
|
|
|
+func (s *Service) ReadIdentity(req request.ManualIdentityRequest) (*responseModel.IdentityResponse, error) {
|
|
|
+ port := req.Port
|
|
|
+ if port == 0 {
|
|
|
+ port = defaultConfigPort
|
|
|
+ }
|
|
|
+ ip, err := ValidateAddress(req.IP, port)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ client := s.httpClient(ip, "")
|
|
|
+ httpReq, _ := http.NewRequest(http.MethodGet, endpoint(ip, port, identityPath), nil)
|
|
|
+ httpReq.Header.Set("Accept", "application/json")
|
|
|
+ resp, err := client.Do(httpReq)
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("HTTPS 身份读取失败: %w", err)
|
|
|
+ }
|
|
|
+ defer resp.Body.Close()
|
|
|
+ body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024+1))
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("读取设备身份失败: %w", err)
|
|
|
+ }
|
|
|
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
|
+ return nil, fmt.Errorf("设备身份接口返回 HTTP %d", resp.StatusCode)
|
|
|
+ }
|
|
|
+ var identity deviceIdentity
|
|
|
+ if err := decodeResponse(body, &identity); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ if strings.TrimSpace(identity.DeviceID) == "" {
|
|
|
+ return nil, errors.New("设备未返回永久设备 ID")
|
|
|
+ }
|
|
|
+ certFingerprint := ""
|
|
|
+ if len(resp.TLS.PeerCertificates) > 0 {
|
|
|
+ hash := sha256.Sum256(resp.TLS.PeerCertificates[0].Raw)
|
|
|
+ certFingerprint = "SHA256:" + strings.ToUpper(hex.EncodeToString(hash[:]))
|
|
|
+ }
|
|
|
+ if identity.CertificateFingerprint != "" && normalizeFingerprint(identity.CertificateFingerprint) != normalizeFingerprint(certFingerprint) {
|
|
|
+ return nil, fmt.Errorf("设备返回的证书指纹与 HTTPS 证书不匹配: received=%q expected=%s", identity.CertificateFingerprint, certFingerprint)
|
|
|
+ }
|
|
|
+ requestID := uuid.Must(uuid.NewV4()).String()
|
|
|
+ expiredAt := time.Now().Add(10 * time.Minute)
|
|
|
+ payload, _ := json.Marshal(identity)
|
|
|
+ record := &dao.DeviceDiscovery{
|
|
|
+ RequestID: requestID, DeviceID: identity.DeviceID, DeviceCode: identity.DeviceCode,
|
|
|
+ DeviceType: identity.DeviceType, DeviceModel: identity.DeviceModel, FirmwareVersion: identity.FirmwareVersion,
|
|
|
+ SourceIP: ip.String(), ConfigPort: port,
|
|
|
+ Payload: string(payload), CertificateFingerprint: certFingerprint,
|
|
|
+ PublicKeyFingerprint: identity.PublicKeyFingerprint,
|
|
|
+ PairingRequired: identity.PairingRequired, ExpiredAt: expiredAt, ProvisionStatus: "identity_unverified",
|
|
|
+ ProvisionMethod: req.ProvisionMethod,
|
|
|
+ }
|
|
|
+ if record.ProvisionMethod == "" {
|
|
|
+ record.ProvisionMethod = "manual_ip"
|
|
|
+ }
|
|
|
+ if record.ProvisionMethod == "auto_udp" {
|
|
|
+ record.SignatureVerified = true
|
|
|
+ }
|
|
|
+ if err := s.repo.CreateDiscovery(record); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return &responseModel.IdentityResponse{
|
|
|
+ RequestID: requestID, DeviceID: identity.DeviceID, DeviceCode: identity.DeviceCode,
|
|
|
+ DeviceType: identity.DeviceType, DeviceModel: identity.DeviceModel, FirmwareVersion: identity.FirmwareVersion,
|
|
|
+ IP: ip.String(), ConfigPort: port, CertificateFingerprint: certFingerprint,
|
|
|
+ PublicKeyFingerprint: identity.PublicKeyFingerprint, PairingRequired: identity.PairingRequired,
|
|
|
+ ExpiredAt: expiredAt, ProvisionStatus: "identity_unverified",
|
|
|
+ }, nil
|
|
|
+}
|
|
|
+
|
|
|
+// VerifyIdentity 固定设备证书指纹并通过设备配对接口确认一次性配对码。
|
|
|
+func (s *Service) VerifyIdentity(deviceID string, req request.VerifyRequest) (*responseModel.IdentityResponse, error) {
|
|
|
+ record, err := s.repo.LatestDiscovery(deviceID)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ if normalizeFingerprint(req.Fingerprint) != normalizeFingerprint(record.CertificateFingerprint) {
|
|
|
+ return nil, errors.New("证书指纹不匹配")
|
|
|
+ }
|
|
|
+ if record.PairingRequired && strings.TrimSpace(req.PairingCode) == "" {
|
|
|
+ return nil, errors.New("设备要求输入一次性配对码")
|
|
|
+ }
|
|
|
+ if record.PairingRequired {
|
|
|
+ ip := net.ParseIP(record.SourceIP)
|
|
|
+ body, _ := json.Marshal(map[string]string{"pairing_code": req.PairingCode})
|
|
|
+ client := s.httpClient(ip, record.CertificateFingerprint)
|
|
|
+ httpReq, _ := http.NewRequest(http.MethodPost, endpoint(ip, record.ConfigPort, "/api/v1/pair"), bytes.NewReader(body))
|
|
|
+ httpReq.Header.Set("Content-Type", "application/json")
|
|
|
+ resp, callErr := client.Do(httpReq)
|
|
|
+ if callErr != nil {
|
|
|
+ return nil, fmt.Errorf("配对码校验失败: %w", callErr)
|
|
|
+ }
|
|
|
+ defer resp.Body.Close()
|
|
|
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
|
+ return nil, fmt.Errorf("配对码校验失败,设备返回 HTTP %d", resp.StatusCode)
|
|
|
+ }
|
|
|
+ }
|
|
|
+ now := time.Now()
|
|
|
+ record.Verified = true
|
|
|
+ record.VerifiedAt = &now
|
|
|
+ record.ProvisionStatus = record.ProvisionMethod + "_verified"
|
|
|
+ if err := s.repo.SaveDiscovery(record); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ var identity deviceIdentity
|
|
|
+ _ = json.Unmarshal([]byte(record.Payload), &identity)
|
|
|
+ return &responseModel.IdentityResponse{
|
|
|
+ RequestID: record.RequestID, DeviceID: record.DeviceID, DeviceCode: identity.DeviceCode,
|
|
|
+ DeviceType: identity.DeviceType, DeviceModel: identity.DeviceModel, FirmwareVersion: identity.FirmwareVersion,
|
|
|
+ IP: record.SourceIP, ConfigPort: record.ConfigPort, CertificateFingerprint: record.CertificateFingerprint,
|
|
|
+ PublicKeyFingerprint: identity.PublicKeyFingerprint, PairingRequired: record.PairingRequired,
|
|
|
+ Verified: true, ExpiredAt: record.ExpiredAt, ProvisionStatus: record.ProvisionStatus,
|
|
|
+ }, nil
|
|
|
+}
|
|
|
+
|
|
|
+type provisionPayload struct {
|
|
|
+ Schema string `json:"schema"`
|
|
|
+ RequestID string `json:"request_id"`
|
|
|
+ DeviceID string `json:"device_id"`
|
|
|
+ DeviceCode string `json:"device_code"`
|
|
|
+ MQTT struct {
|
|
|
+ Host string `json:"host"`
|
|
|
+ Port int `json:"port"`
|
|
|
+ TLS bool `json:"tls"`
|
|
|
+ ClientID string `json:"client_id"`
|
|
|
+ } `json:"mqtt"`
|
|
|
+ Route struct {
|
|
|
+ ParkingLotID uint `json:"parking_lot_id"`
|
|
|
+ BoothID uint `json:"booth_id"`
|
|
|
+ ChannelID uint `json:"channel_id"`
|
|
|
+ Direction string `json:"direction"`
|
|
|
+ } `json:"route"`
|
|
|
+ ImageUpload struct {
|
|
|
+ URL string `json:"url"`
|
|
|
+ MaxBytes int64 `json:"max_bytes"`
|
|
|
+ } `json:"image_upload"`
|
|
|
+}
|
|
|
+
|
|
|
+const deviceImageUploadMaxBytes int64 = 8 * 1024 * 1024
|
|
|
+
|
|
|
+func deviceImageUploadURL(host string, port int) string {
|
|
|
+ return "http://" + net.JoinHostPort(host, strconv.Itoa(port)) + "/device-images/upload"
|
|
|
+}
|
|
|
+
|
|
|
+// Provision 校验业务绑定后下发配置,并创建/更新 MQTT 设备记录。
|
|
|
+func (s *Service) Provision(deviceID string, req request.ProvisionRequest) (*responseModel.ProvisionStatusResponse, error) {
|
|
|
+ record, err := s.repo.LatestDiscovery(deviceID)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ if !record.Verified {
|
|
|
+ return nil, errors.New("设备身份尚未确认")
|
|
|
+ }
|
|
|
+ if global.GVA_DB == nil {
|
|
|
+ return nil, errors.New("数据库未初始化")
|
|
|
+ }
|
|
|
+ var channel dao.Channel
|
|
|
+ if err := global.GVA_DB.Preload("Booth").Where("id = ?", req.ChannelID).First(&channel).Error; err != nil {
|
|
|
+ return nil, errors.New("通道不存在")
|
|
|
+ }
|
|
|
+ if channel.BoothID != req.BoothID || channel.ParkingLotID != req.ParkingLotID || channel.Booth == nil || channel.Booth.ParkingLotID != req.ParkingLotID {
|
|
|
+ return nil, errors.New("停车场、岗亭和通道归属不一致")
|
|
|
+ }
|
|
|
+ // device_code has a database-wide unique index. Include soft-deleted rows in
|
|
|
+ // this lookup; otherwise a deleted record can pass the pre-check and fail
|
|
|
+ // later with SQLite 2067 during INSERT.
|
|
|
+ existing, existingFound, err := findProvisionReader(global.GVA_DB, deviceID, req.DeviceCode)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ cfg := global.GVA_CONFIG.Mqtt
|
|
|
+ if strings.TrimSpace(cfg.AdvertisedHost) == "" || net.ParseIP(cfg.AdvertisedHost) == nil {
|
|
|
+ return nil, errors.New("未配置可供边缘设备访问的 MQTT advertised-host")
|
|
|
+ }
|
|
|
+ if _, err := ValidateAddress(cfg.AdvertisedHost, cfg.AdvertisedPort); err != nil {
|
|
|
+ return nil, fmt.Errorf("MQTT advertised 地址无效: %w", err)
|
|
|
+ }
|
|
|
+ requestID := uuid.Must(uuid.NewV4()).String()
|
|
|
+ payload := provisionPayload{Schema: "provision.request.v1", RequestID: requestID, DeviceID: deviceID, DeviceCode: req.DeviceCode}
|
|
|
+ payload.MQTT.Host, payload.MQTT.Port, payload.MQTT.TLS, payload.MQTT.ClientID = cfg.AdvertisedHost, cfg.AdvertisedPort, cfg.TLSEnabled, req.DeviceCode
|
|
|
+ payload.Route.ParkingLotID, payload.Route.BoothID, payload.Route.ChannelID, payload.Route.Direction = req.ParkingLotID, req.BoothID, req.ChannelID, req.Direction
|
|
|
+ payload.ImageUpload.URL, payload.ImageUpload.MaxBytes = deviceImageUploadURL(cfg.AdvertisedHost, global.GVA_CONFIG.System.Addr), deviceImageUploadMaxBytes
|
|
|
+ body, _ := json.Marshal(payload)
|
|
|
+ ip := net.ParseIP(record.SourceIP)
|
|
|
+ client := s.httpClient(ip, record.CertificateFingerprint)
|
|
|
+ httpReq, _ := http.NewRequest(http.MethodPost, endpoint(ip, record.ConfigPort, provisionPath), bytes.NewReader(body))
|
|
|
+ httpReq.Header.Set("Content-Type", "application/json")
|
|
|
+ resp, err := client.Do(httpReq)
|
|
|
+ if err != nil {
|
|
|
+ return nil, fmt.Errorf("HTTPS 配置下发失败: %w", err)
|
|
|
+ }
|
|
|
+ defer resp.Body.Close()
|
|
|
+ responseBody, _ := io.ReadAll(io.LimitReader(resp.Body, 64*1024+1))
|
|
|
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
|
+ return nil, fmt.Errorf("设备拒绝配置,HTTP %d", resp.StatusCode)
|
|
|
+ }
|
|
|
+ var accepted struct {
|
|
|
+ Result string `json:"result"`
|
|
|
+ }
|
|
|
+ if err := decodeResponse(responseBody, &accepted); err != nil {
|
|
|
+ return nil, fmt.Errorf("设备配置响应无效: %w", err)
|
|
|
+ }
|
|
|
+ if accepted.Result != "accepted" {
|
|
|
+ return nil, errors.New("设备未接受 MQTT 配置")
|
|
|
+ }
|
|
|
+ now := time.Now()
|
|
|
+ if !existingFound {
|
|
|
+ existing = dao.UHFReader{DeviceID: deviceID, DeviceCode: req.DeviceCode, DeviceName: req.DeviceName, DeviceType: req.DeviceType, ConnectType: dao.ConnectTypeMQTT, ChannelID: req.ChannelID, ParkingLotID: req.ParkingLotID}
|
|
|
+ }
|
|
|
+ // Restore a soft-deleted row before saving the new binding. Save must be
|
|
|
+ // Unscoped so GORM does not silently exclude the historical row.
|
|
|
+ existing.DeletedAt = gorm.DeletedAt{}
|
|
|
+ existing.DeviceCode, existing.DeviceName, existing.DeviceType = req.DeviceCode, req.DeviceName, req.DeviceType
|
|
|
+ existing.ConnectType, existing.ChannelID, existing.ParkingLotID = dao.ConnectTypeMQTT, req.ChannelID, req.ParkingLotID
|
|
|
+ existing.IPAddress, existing.ConfigPort = record.SourceIP, record.ConfigPort
|
|
|
+ existing.DeviceModel, existing.FirmwareVersion = record.DeviceModel, record.FirmwareVersion
|
|
|
+ existing.DevicePublicKey, existing.ProvisionStatus, existing.ProvisionMethod, existing.ProvisionError = record.CertificateFingerprint, "provisioned", record.ProvisionMethod, ""
|
|
|
+ existing.IdentityVerifiedAt = record.VerifiedAt
|
|
|
+ existing.ProvisionedAt = &now
|
|
|
+ if !existingFound {
|
|
|
+ if err := global.GVA_DB.Create(&existing).Error; err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ } else if err := global.GVA_DB.Unscoped().Save(&existing).Error; err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return &responseModel.ProvisionStatusResponse{DeviceID: deviceID, DeviceCode: req.DeviceCode, ProvisionStatus: "provisioned", ProvisionMethod: record.ProvisionMethod, DeviceStatus: existing.Status, IP: record.SourceIP, ConfigPort: record.ConfigPort, CertificateFinger: record.CertificateFingerprint, VerifiedAt: record.VerifiedAt, ProvisionedAt: &now}, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (s *Service) Status(deviceID string) (*responseModel.ProvisionStatusResponse, error) {
|
|
|
+ reader, err := s.repo.ReaderByDeviceID(deviceID)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ if reader == nil {
|
|
|
+ return nil, errors.New("设备尚未完成配置")
|
|
|
+ }
|
|
|
+ return &responseModel.ProvisionStatusResponse{DeviceID: reader.DeviceID, DeviceCode: reader.DeviceCode, ProvisionStatus: reader.ProvisionStatus, ProvisionMethod: reader.ProvisionMethod, DeviceStatus: reader.Status, ProvisionError: reader.ProvisionError, IP: reader.IPAddress, ConfigPort: reader.ConfigPort, CertificateFinger: reader.DevicePublicKey, VerifiedAt: reader.IdentityVerifiedAt, ProvisionedAt: reader.ProvisionedAt}, nil
|
|
|
+}
|