CreateTrip

ProfileManager,CarManager,POIManager方法的实现先丢一边,需要考虑的是实际业务:

  • 加入人的位置来防止恶意攻击;

  • 行程必须是在车辆开锁前创建;

  • 使用后台开锁,因为不管开锁是否成功,行程都已经创建了;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type Service struct {
CarManager CarManager
ProfileManager ProfileManager
POIManager POIManager
Mongo *dao.Mongo
Logger *zap.Logger
}

// ProfileManager defines the ACL(Anti Corruptino Layer)
// for profile verification logic.
type ProfileManager interface {
Verigy(context.Context, id.AccountID) (id.IdentiTyID, error)
}

type CarManager interface {
// 加入人的位置
Verfigy(context.Context, id.CarID, *rentalpb.Location) error
Unlock(context.Context, id.CarID) error
}

// resolves POI(Point Of Interest)
type POIManager interface {
Resolve(context.Context, *rentalpb.Location) (string, error)
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
func (s *Service) CreateTrip(c context.Context, req *rentalpb.CreateTripRequest) (*rentalpb.TripEntity, error) {
aid, err := auth.AccountIDFromContext(c)
if err != nil {
return nil, err
}
// 验证驾驶证身份
iID, err := s.ProfileManager.Verigy(c, aid)
if err != nil {
return nil, status.Errorf(codes.FailedPrecondition, err.Error())
}
// 检查车辆状态,车子可能已经被租了
carID := id.CarID(req.CarId)
err = s.CarManager.Verfigy(c, carID, req.Start)
if err != nil {
return nil, status.Errorf(codes.FailedPrecondition, err.Error())
}
// 创建行程:写入数据库,开始计费
poi, err := s.POIManager.Resolve(c, req.Start)
if err!=nil{
s.Logger.Info("cannot resolve poi",zap.Stringer("location", req.Start),zap.Error(err))
}
ls := &rentalpb.LocationStatus{
Location: req.Start,
PoiName: poi,
}
tr, err:=s.Mongo.CreateTrip(c, &rentalpb.Trip{
AccountID: aid.String(),
CarID: carID.String(),
IdentityId: iID.String(),
Status: rentalpb.TripStatus_IN_PROGRES,
Start: ls,
Current: ls,
})

if err != nil{
s.Logger.Warn("cannot create trip", zap.Error(err))
return nil,status.Error(codes.AlreadyExists,"")
}

// 车辆开锁
// 后台开锁,因为不管开锁是否成功,行程都已经创建了
go func() {
err = s.CarManager.Unlock(context.Background(), carID)
if err !=nil{
s.Logger.Error("cannot unlock car",zap.Error(err))
}
}()
return &rentalpb.TripEntity{
Id: tr.ID.Hex(),
Trip: tr.Trip,
}, nil
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
func TestCreateTrip(t *testing.T) {
c := auth.ContestWithAccontId(context.Background(), id.AccountID("accountID1"))
mc, err := mongotesting.NewClient(c)
if err != nil {
t.Fatalf("cannot create mongo client: %v", err)
}
logger, err := zap.NewDevelopment()
if err != nil {
t.Fatalf("cannot create logger : %v", err)
}
pm := &profileManager{}
cm := &carManager{}
s := &Service{
ProfileManager: pm,
CarManager: cm,
POIManager: &poi.Manager{},
Mongo: dao.NewMongo(mc.Database("coolcar")),
Logger: logger,
}
req := &rentalpb.CreateTripRequest{
CarId: "car1",
Start: &rentalpb.Location{
Latitude: 32.12,
Longitude: 114.2555,
},
}
pm.iID = "identity1"
golden := `{"accountID":"accountID1","carID":"car1","start":{"location":{"latitude":32.12,"longitude":114.2555},"poi_name":"天安门"},"current":{"location":{"latitude":32.12,"longitude":114.2555},"poi_name":"天安门"},"status":1,"identity_id":"identity1"}`
cases := []struct {
name string
tripID string
profileErr error
carVerifyErr error
carUnlockErr error
want string
wantErr bool
}{
{
name: "normal_create",
tripID: "662c60186800fc9e2ca1480d",
want: golden,
},
{
name: "profile_err",
tripID: "662c60186800fc9e2ca14801",
profileErr: fmt.Errorf("profile"),
wantErr: true,
},
{
name: "car_verify_err",
tripID: "662c60186800fc9e2ca14802",
carVerifyErr: fmt.Errorf("verify"),
wantErr: true,
},
{
name: "car_unlock_err",
tripID: "662c60186800fc9e2ca14803",
carUnlockErr: fmt.Errorf("unlock"),
wantErr: false, // 解锁失败,创建trip还是成功的
want: golden,
},
}

for _, cc := range cases {
t.Run(cc.name, func(t *testing.T) {
mgo.NewObjIDWithValue(id.TripID(cc.tripID))
pm.err = cc.profileErr
cm.unlockErr = cc.carUnlockErr
cm.verifyErr = cc.carVerifyErr
res, err := s.CreateTrip(c, req)
if cc.wantErr {
if err == nil {
t.Errorf("want error got none")
} else {
return
}
}

if err != nil {
t.Errorf("error creating trip: %v", err)
return
}
if res.Id != cc.tripID {
t.Errorf("incorrect id; want %q,got %q", cc.tripID, res.Id)
}
b, err := json.Marshal(res.Trip)
if err != nil {
t.Errorf("cannot marshal response:%v", err)
}
tripnStr := string(b)
if cc.want != tripnStr {
t.Errorf("incorrect response:want %s , got %s", cc.want, tripnStr)
}

})
}
}

type profileManager struct {
iID id.IdentiTyID
err error
}

func (p *profileManager) Verify(context.Context, id.AccountID) (id.IdentiTyID, error) {
return p.iID, p.err
}

type carManager struct {
verifyErr error
unlockErr error
}

func (c *carManager) Verfigy(context.Context, id.CarID, *rentalpb.Location) error {
return c.verifyErr
}
func (c *carManager) Unlock(context.Context, id.CarID) error {
return c.unlockErr
}

func TestMain(m *testing.M) {
os.Exit(mongotesting.RunWithMongoInDocker(m))
}

前后端联调

步骤一:创建实现carManager,profileManager,poiManager

/rental/trip/client下创建car,poi,profile文件夹,用来实现这三个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package car

import (
"context"
rentalpb "coolcar/rental/api/gen/v1"
"coolcar/shared/id"
)

type Manager struct {
}

func (c *Manager) Verfigy(context.Context, id.CarID, *rentalpb.Location) error {
return nil
}
func (c *Manager) Unlock(context.Context, id.CarID) error {
return nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package poi

import (
"context"
rentalpb "coolcar/rental/api/gen/v1"
"hash/fnv"

"google.golang.org/protobuf/proto"
)

var poi = []string{
"中关村",
"天安门",
}

type Manager struct {
}

func (*Manager) Resolve(c context.Context, loc *rentalpb.Location) (string, error) {
b, err := proto.Marshal(loc)
if err != nil {
return "", err
}
h := fnv.New32()
h.Write(b)
return poi[int(h.Sum32())%len(poi)], nil
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package profile

import (
"context"
"coolcar/shared/id"
)

type Manager struct {
}

func (p *Manager) Verify(context.Context, id.AccountID) (id.IdentiTyID, error) {
return id.IdentiTyID("account1"), nil
}

步骤二:修改main.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func main() {
logger, err := zap.NewDevelopment()
if err != nil {
log.Fatalf("cannot create logger : %v", err)
}
c := context.Background()
mgURI := "mongodb://localhost:27017/coolcar?readPreference=primary&ssl=false"
mongoClient, err := mongo.Connect(c, options.Client().ApplyURI(mgURI))
if err != nil {
logger.Fatal("cannot connect mongodb: %v", zap.Error(err))
}
err = server.RunGRPCServer(&server.GRPCConfig{
Name: "rental",
Addr: ":8082",
Logger: logger,
AuthPublicKeyFile: "shared/auth/public.key",
RegisterFunc: func(s *grpc.Server) {
rentalpb.RegisterTripServiceServer(s, &trip.Service{
CarManager: &car.Manager{},
ProfileManager: &profile.Manager{},
POIManager: &poi.Manager{},
Mongo: dao.NewMongo(mongoClient.Database("coolcar")),
Logger: logger,
})
},
})
if err != nil {
logger.Fatal("cannot server: %v", zap.Error(err))
}
}

步骤三:修改前端的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
onUnlockTap(){
wx.getLocation({
type: 'gcj02',
success: async loc => {
if (!this.carID) {
console.error('no carID specified')
return
}
const trip = await TripService.CreateTrip({
carId: this.carID,
start: {
latitude:loc.latitude,
longitude:loc.longitude,
},
})
wx.showLoading({
title:'开锁中',
mask: true,
})
if (!trip.id){
console.error(trip)
console.error("no tripID in response",trip)
return
}
setTimeout(() => {
wx.redirectTo({
// url:`/pages/driving/driving?trip-id=${tripId}`,
url: routing.driving({
trip_id: trip.id!
}),
complete: ()=>{
wx.hideLoading()
}
})
},2000)

},
fail: ()=>{
wx.showToast({
icon:"none",
title:'前往右上角设置界面授权获取位置信息',
})
}
})
},

更新和查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
func (s *Service) GetTrip(c context.Context, req *rentalpb.GetTripRequest) (*rentalpb.Trip, error) {
fmt.Print("start GetTrip")
aid, err := auth.AccountIDFromContext(c)
if err != nil {
return nil, err
}
tr, err := s.Mongo.GetTrip(c, id.TripID(req.Id), aid)
if err != nil {
return nil, status.Error(codes.NotFound, "")
}
return tr.Trip, nil
}

func (s *Service) GetTrips(c context.Context, req *rentalpb.GetTripsRequest) (*rentalpb.GetTripsResponse, error) {
aid, err := auth.AccountIDFromContext(c)
if err != nil {
return nil, err
}

trips, err := s.Mongo.GetTrips(c, aid, req.Status)
if err != nil {
s.Logger.Error("cannot get trips", zap.Error(err))
return nil, status.Error(codes.Internal, "")
}
res := &rentalpb.GetTripsResponse{}
for _, tr := range trips {
res.Trips = append(res.Trips, &rentalpb.TripEntity{
Id: tr.ID.Hex(),
Trip: tr.Trip,
})
}
return res, nil
}

func (s *Service) UpdateTrip(c context.Context, req *rentalpb.UpdateTripRequest) (*rentalpb.Trip, error) {
aid, err := auth.AccountIDFromContext(c)
if err != nil {
return nil, err
}
tr, err := s.Mongo.GetTrip(c, id.TripID(req.Id), aid)
if err != nil {
return nil, status.Error(codes.NotFound, "")
}
if tr.Trip.Current == nil {
s.Logger.Error("trip without current set", zap.String("id", tr.ID.String()))
return nil, status.Error(codes.Internal, "")
}
// 考虑到车子不动的情况
cur := tr.Trip.Current.Location
if req.Current != nil {
cur = req.Current
}
ls := s.calcCurrentStatus(c, tr.Trip.Current, cur)
tr.Trip.Current = ls
if req.EndTrip {
tr.Trip.End = tr.Trip.Current
tr.Trip.Status = rentalpb.TripStatus_FINISHED
}
err = s.Mongo.UpdateTrip(c, id.TripID(req.Id), aid, tr.UpdatedAt, tr.Trip)
if err != nil {
return nil, status.Error(codes.Aborted, "")
}
return tr.Trip, nil
}

var nowFunc = func() int64 {
return time.Now().Unix()
}

const centsPerSec = 0.7
const kmPerSec = 0.02

func (s *Service) calcCurrentStatus(c context.Context, last *rentalpb.LocationStatus, cur *rentalpb.Location) *rentalpb.LocationStatus {
now := nowFunc()
elapsedSec := float64(now - last.TimestampSec)

poi, err := s.POIManager.Resolve(c, cur)
if err != nil {
s.Logger.Info("cannot resolve poi", zap.Stringer("location", cur), zap.Error(err))
}
return &rentalpb.LocationStatus{
Location: cur,
FeeCent: last.FeeCent + int32(centsPerSec*elapsedSec*2*rand.Float64()),
KmDriven: last.KmDriven + kmPerSec*elapsedSec*2*rand.Float64(),
TimestampSec: now,
PoiName: poi,
}
}

测试trip的整个流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
func TestTripLifecycle(t *testing.T) {
c := auth.ContestWithAccontId(context.Background(), id.AccountID("account_id_lifecycle"))
s := newService(c, t, &profileManager{}, &carManager{})
tid := id.TripID("621c60186800fc9e2ca14801")
mgo.NewObjIDWithValue(tid)
cases := []struct {
name string
now int64
op func() (*rentalpb.Trip, error)
want string
}{
{
name: "create_trip",
now: 10000,
op: func() (*rentalpb.Trip, error) {
e, err := s.CreateTrip(c, &rentalpb.CreateTripRequest{
Start: &rentalpb.Location{
Latitude: 34,
Longitude: 122,
},
CarId: "car123",
})
if err != nil {
return nil, err
}
return e.Trip, nil
},
want: `{"account_id":"account_id_lifecycle","car_id":"car123","start":{"location":{"latitude":34,"longitude":122},"poi_name":"中关村","timestamp_sec":10000},"current":{"location":{"latitude":34,"longitude":122},"poi_name":"中关村","timestamp_sec":10000},"status":1}`,
},
{
name: "update_trip",
now: 20000,
op: func() (*rentalpb.Trip, error) {
return s.UpdateTrip(c, &rentalpb.UpdateTripRequest{
Id: tid.String(),
Current: &rentalpb.Location{
Latitude: 28.2,
Longitude: 122,
},
})
},
want: `{"account_id":"account_id_lifecycle","car_id":"car123","start":{"location":{"latitude":34,"longitude":122},"poi_name":"中关村","timestamp_sec":10000},"current":{"location":{"latitude":28.2,"longitude":122},"fee_cent":9116,"km_driven":359.56460921309167,"poi_name":"天安门","timestamp_sec":20000},"status":1}`,
},
{
name: "finish_trip",
now: 30000,
op: func() (*rentalpb.Trip, error) {
return s.UpdateTrip(c, &rentalpb.UpdateTripRequest{
Id: tid.String(),
EndTrip: true,
})
},
want: `{"account_id":"account_id_lifecycle","car_id":"car123","start":{"location":{"latitude":34,"longitude":122},"poi_name":"中关村","timestamp_sec":10000},"current":{"location":{"latitude":28.2,"longitude":122},"fee_cent":17638,"km_driven":480.5067823692563,"poi_name":"天安门","timestamp_sec":30000},"end":{"location":{"latitude":28.2,"longitude":122},"fee_cent":17638,"km_driven":480.5067823692563,"poi_name":"天安门","timestamp_sec":30000},"status":2}`,
},
{
name: "query_trip",
now: 40000,
op: func() (*rentalpb.Trip, error) {
return s.GetTrip(c, &rentalpb.GetTripRequest{
Id: tid.String(),
})
},
want: `{"account_id":"account_id_lifecycle","car_id":"car123","start":{"location":{"latitude":34,"longitude":122},"poi_name":"中关村","timestamp_sec":10000},"current":{"location":{"latitude":28.2,"longitude":122},"fee_cent":17638,"km_driven":480.5067823692563,"poi_name":"天安门","timestamp_sec":30000},"end":{"location":{"latitude":28.2,"longitude":122},"fee_cent":17638,"km_driven":480.5067823692563,"poi_name":"天安门","timestamp_sec":30000},"status":2}`,
},
}
rand.Seed(1234)
for _, cc := range cases {
nowFunc = func() int64 {
return cc.now
}
trip, err := cc.op()
if err != nil {
t.Errorf("%s: operation failed:%v", cc.name, err)
continue
}
b, err := json.Marshal(trip)
if err != nil {
t.Errorf("%s:failed marshalling response: %v", cc.name, err)
}
got := string(b)
if cc.want != got {
t.Errorf("incorrect response:want %s , got %s", cc.want, got)
}
}
}
func newService(c context.Context, t *testing.T, pm ProfileManager, cm CarManager) *Service {
mc, err := mongotesting.NewClient(c)
if err != nil {
t.Fatalf("cannot create mongo client: %v", err)
}
logger, err := zap.NewDevelopment()
if err != nil {
t.Fatalf("cannot create logger : %v", err)
}
db := mc.Database("coolcar")
mongotesting.SetupIndex(c, db)
return &Service{
ProfileManager: pm,
CarManager: cm,
POIManager: &poi.Manager{},
Mongo: dao.NewMongo(db),
Logger: logger,
}
}