feat: add backbone_code to Tower for road routing waypoints

Towers can now be explicitly assigned to a backbone by setting
backbone_code. This field is included in create/update DTOs and
the response. The backbone OSRM routing merges these tower waypoints
with Closure waypoints, sorted by distance from OTB-start.

Run once on DB: ALTER TABLE towers ADD COLUMN backbone_code VARCHAR(255);

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
unknown 2026-04-12 10:56:57 +07:00
parent 9ae9de471d
commit 4461aff5c1
5 changed files with 37 additions and 27 deletions

View File

@ -8,7 +8,8 @@ type TowerDTO struct {
TowerCode string `json:"tower_code" validate:"required"` TowerCode string `json:"tower_code" validate:"required"`
Longitude float64 `json:"longitude" validate:"required"` Longitude float64 `json:"longitude" validate:"required"`
Latitude float64 `json:"latitude" validate:"required"` Latitude float64 `json:"latitude" validate:"required"`
ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable ExternalTower *bool `json:"external_tower,omitempty"`
BackboneCode *string `json:"backbone_code,omitempty"`
} }
type UpdateTowerDTO struct { type UpdateTowerDTO struct {
@ -17,8 +18,9 @@ type UpdateTowerDTO struct {
TowerCode *string `json:"tower_code,omitempty"` TowerCode *string `json:"tower_code,omitempty"`
Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"` Longitude *float64 `json:"longitude,omitempty" validate:"omitempty,longitude"`
Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"` Latitude *float64 `json:"latitude,omitempty" validate:"omitempty,latitude"`
ExternalTower *bool `json:"external_tower,omitempty"` // Make nullable ExternalTower *bool `json:"external_tower,omitempty"`
ImageURL *string `json:"image_url,omitempty"` ImageURL *string `json:"image_url,omitempty"`
BackboneCode *string `json:"backbone_code,omitempty"`
} }
// Add to model/dto/req/tower_dto.go // Add to model/dto/req/tower_dto.go

View File

@ -14,7 +14,8 @@ type TowerResponse struct {
Address string `json:"address"` Address string `json:"address"`
ImageURL string `json:"image_url"` ImageURL string `json:"image_url"`
ImageURLs []string `json:"image_urls"` // Store multiple images as JSONB ImageURLs []string `json:"image_urls"` // Store multiple images as JSONB
ExternalTower *bool `json:"external_tower"` // Make nullable ExternalTower *bool `json:"external_tower"`
BackboneCode *string `json:"backbone_code,omitempty"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
} }

View File

@ -15,6 +15,7 @@ type Tower struct {
ImageURL string `json:"image_url"` // Keep for backward compatibility ImageURL string `json:"image_url"` // Keep for backward compatibility
ImageURLs StringSlice `json:"image_urls" gorm:"type:jsonb"` // Multiple images ImageURLs StringSlice `json:"image_urls" gorm:"type:jsonb"` // Multiple images
ExternalTower *bool `json:"external_tower,omitempty"` ExternalTower *bool `json:"external_tower,omitempty"`
BackboneCode *string `json:"backbone_code,omitempty" gorm:"column:backbone_code"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`

View File

@ -79,9 +79,10 @@ func (u *towerUsecase) PostWithMultipleImages(tower req.TowerDTO, imageFiles []*
TowerCode: tower.TowerCode, TowerCode: tower.TowerCode,
Longitude: tower.Longitude, Longitude: tower.Longitude,
Latitude: tower.Latitude, Latitude: tower.Latitude,
ImageURL: primaryImageURL, // Primary image ImageURL: primaryImageURL,
ImageURLs: entity.StringSlice(imageURLs), // All images ImageURLs: entity.StringSlice(imageURLs),
ExternalTower: tower.ExternalTower, ExternalTower: tower.ExternalTower,
BackboneCode: tower.BackboneCode,
CreatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(), UpdatedAt: time.Now(),
} }
@ -115,6 +116,9 @@ func (u *towerUsecase) UpdateTowerWithMultipleImages(id uuid.UUID, tower req.Upd
if tower.ImageURL != nil { if tower.ImageURL != nil {
updates["ImageURL"] = *tower.ImageURL updates["ImageURL"] = *tower.ImageURL
} }
if tower.BackboneCode != nil {
updates["BackboneCode"] = *tower.BackboneCode
}
// Handle multiple image uploads // Handle multiple image uploads
if len(imageFiles) > 0 { if len(imageFiles) > 0 {
@ -196,15 +200,16 @@ func (u *towerUsecase) Post(tower req.TowerDTO, imageFile *multipart.FileHeader)
} }
newTower := entity.Tower{ newTower := entity.Tower{
ID: uuid.New(), ID: uuid.New(),
DeviceID: tower.DeviceID, // Now nullable DeviceID: tower.DeviceID,
TowerCode: tower.TowerCode, TowerCode: tower.TowerCode,
Longitude: tower.Longitude, Longitude: tower.Longitude,
Latitude: tower.Latitude, Latitude: tower.Latitude,
ImageURL: imageURL, ImageURL: imageURL,
ExternalTower: tower.ExternalTower, // Now nullable ExternalTower: tower.ExternalTower,
CreatedAt: time.Now(), BackboneCode: tower.BackboneCode,
UpdatedAt: time.Now(), CreatedAt: time.Now(),
UpdatedAt: time.Now(),
} }
return u.towerRepo.Post(newTower) return u.towerRepo.Post(newTower)

View File

@ -37,7 +37,8 @@ func ConvertToTowerResponses(towers []entity.Tower, geocoder service.GeocodingSe
Address: address, Address: address,
ImageURL: tower.ImageURL, ImageURL: tower.ImageURL,
ImageURLs: allImageURLs, // All images ImageURLs: allImageURLs, // All images
ExternalTower: tower.ExternalTower, // Now nullable ExternalTower: tower.ExternalTower,
BackboneCode: tower.BackboneCode,
CreatedAt: tower.CreatedAt, CreatedAt: tower.CreatedAt,
UpdatedAt: tower.UpdatedAt, UpdatedAt: tower.UpdatedAt,
} }
@ -62,21 +63,21 @@ func ConvertToTowerIDResponses(tower entity.Tower, geocoder service.GeocodingSer
if tower.Device.DeviceCode != "" { if tower.Device.DeviceCode != "" {
deviceCode = &tower.Device.DeviceCode deviceCode = &tower.Device.DeviceCode
} }
// Get all image URLs
allImageURLs := tower.GetAllImageURLs() allImageURLs := tower.GetAllImageURLs()
towerResp := res.TowerResponse{ towerResp := res.TowerResponse{
ID: tower.ID, ID: tower.ID,
DeviceCode: deviceCode, // Now nullable DeviceCode: deviceCode,
TowerCode: &tower.TowerCode, TowerCode: &tower.TowerCode,
Longitude: tower.Longitude, Longitude: tower.Longitude,
Latitude: tower.Latitude, Latitude: tower.Latitude,
Address: address, Address: address,
ImageURL: tower.ImageURL, ImageURL: tower.ImageURL,
ImageURLs: allImageURLs, // All images ImageURLs: allImageURLs,
ExternalTower: tower.ExternalTower, // Now nullable ExternalTower: tower.ExternalTower,
CreatedAt: tower.CreatedAt, BackboneCode: tower.BackboneCode,
UpdatedAt: tower.UpdatedAt, CreatedAt: tower.CreatedAt,
UpdatedAt: tower.UpdatedAt,
} }
return towerResp, nil return towerResp, nil
} }