// Package service 持有业务逻辑,只依赖 repository 接口,不直接碰 GORM。 // // 计算类逻辑(如油价/油耗/成本)放这里,不放在 handler,便于单测。 package service import ( "context" "strings" "gorm.io/gorm" "github.com/kennethcheng/fuellog-api/internal/dto" "github.com/kennethcheng/fuellog-api/internal/model" "github.com/kennethcheng/fuellog-api/internal/repository" "github.com/kennethcheng/fuellog-api/pkg/apperror" ) // VehicleService 车辆业务逻辑。 type VehicleService interface { Create(ctx context.Context, in *dto.VehicleCreate) (*model.Vehicle, error) Get(ctx context.Context, id uint64) (*model.Vehicle, error) List(ctx context.Context) ([]model.Vehicle, error) Update(ctx context.Context, id uint64, in *dto.VehicleUpdate) (*model.Vehicle, error) Delete(ctx context.Context, id uint64) error SetDefault(ctx context.Context, id uint64) (*model.Vehicle, error) } func NewVehicleService(repo repository.VehicleRepository, db *gorm.DB) VehicleService { return &vehicleService{repo: repo, db: db} } type vehicleService struct { repo repository.VehicleRepository db *gorm.DB } func (s *vehicleService) Create(ctx context.Context, in *dto.VehicleCreate) (*model.Vehicle, error) { v := &model.Vehicle{ Name: strings.TrimSpace(in.Name), Brand: strings.TrimSpace(in.Brand), Model: strings.TrimSpace(in.Model), PlateNumber: strings.TrimSpace(in.PlateNumber), FuelType: strings.TrimSpace(in.FuelType), TankCapacity: in.TankCapacity, CurrentOdometer: in.CurrentOdometer, DefaultVehicle: in.DefaultVehicle, } if v.Name == "" { return nil, apperror.New(apperror.CodeInvalidArg, "车辆名称不能为空") } if v.FuelType == "" { return nil, apperror.New(apperror.CodeInvalidArg, "能源类型不能为空") } // 任何显式设为默认的车辆,都要先清掉其它默认(在事务里完成) if v.DefaultVehicle { err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := s.repo.ClearDefault(ctx, tx); err != nil { return err } if err := tx.WithContext(ctx).Create(v).Error; err != nil { return apperror.Wrap(apperror.CodeInternal, "创建车辆失败", err) } return nil }) if err != nil { if appErr, ok := apperror.As(err); ok { return nil, appErr } return nil, err } return v, nil } if err := s.repo.Create(ctx, v); err != nil { return nil, err } return v, nil } func (s *vehicleService) Get(ctx context.Context, id uint64) (*model.Vehicle, error) { return s.repo.Get(ctx, id) } func (s *vehicleService) List(ctx context.Context) ([]model.Vehicle, error) { return s.repo.List(ctx) } func (s *vehicleService) Update(ctx context.Context, id uint64, in *dto.VehicleUpdate) (*model.Vehicle, error) { v, err := s.repo.Get(ctx, id) if err != nil { return nil, err } if in.Name != nil { v.Name = strings.TrimSpace(*in.Name) } if in.Brand != nil { v.Brand = strings.TrimSpace(*in.Brand) } if in.Model != nil { v.Model = strings.TrimSpace(*in.Model) } if in.PlateNumber != nil { v.PlateNumber = strings.TrimSpace(*in.PlateNumber) } if in.FuelType != nil { v.FuelType = strings.TrimSpace(*in.FuelType) } if in.TankCapacity != nil { v.TankCapacity = *in.TankCapacity } if in.CurrentOdometer != nil { v.CurrentOdometer = *in.CurrentOdometer } if v.Name == "" { return nil, apperror.New(apperror.CodeInvalidArg, "车辆名称不能为空") } if v.FuelType == "" { return nil, apperror.New(apperror.CodeInvalidArg, "能源类型不能为空") } // 切换默认车辆同样要在事务里:先清默认,再置新默认 if in.DefaultVehicle != nil && *in.DefaultVehicle && !v.DefaultVehicle { err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := s.repo.ClearDefault(ctx, tx); err != nil { return err } v.DefaultVehicle = true if err := tx.WithContext(ctx).Save(v).Error; err != nil { return apperror.Wrap(apperror.CodeInternal, "更新车辆失败", err) } return nil }) if err != nil { if appErr, ok := apperror.As(err); ok { return nil, appErr } return nil, err } return v, nil } if err := s.repo.Update(ctx, v); err != nil { return nil, err } return v, nil } func (s *vehicleService) Delete(ctx context.Context, id uint64) error { return s.repo.Delete(ctx, id) } func (s *vehicleService) SetDefault(ctx context.Context, id uint64) (*model.Vehicle, error) { // 校验存在 if _, err := s.repo.Get(ctx, id); err != nil { return nil, err } err := s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { if err := s.repo.ClearDefault(ctx, tx); err != nil { return err } return s.repo.SetDefault(ctx, tx, id) }) if err != nil { if appErr, ok := apperror.As(err); ok { return nil, appErr } return nil, err } return s.repo.Get(ctx, id) }