安装腾讯云的对象存储

tencentyun/cos-go-sdk-v5: 腾讯云 COS GO SDK(XML API) (github.com)

对象存储 快速入门 - SDK 文档 - 文档中心 - 腾讯云 (tencent.com)

1
go get -u github.com/tencentyun/cos-go-sdk-v5

永久密钥预签名请求示例

下载请求示例

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
func main() {
u, err := url.Parse("https://coolcar-1309863650.cos.ap-nanjing.myqcloud.com")
if err != nil {
panic(err)
}
// 用于Get Service 查询,默认全地域 service.cos.myqcloud.com
su, _ := url.Parse("https://cos.ap-nanjing.myqcloud.com")
b := &cos.BaseURL{BucketURL: u, ServiceURL: su}
secretID := "AKID2tUpthFWWLuCCxRjgPr2wZv0oLbbKnzu"
secretKey := "g6F9m8D2arqY1C7mAI9k47sNbaIjtoSp"
// 1.永久密钥
client := cos.NewClient(b, &http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: secretID, // 替换为用户的 SecretId,请登录访问管理控制台进行查看和管理,https://console.cloud.tencent.com/cam/capi
SecretKey: secretKey, // 替换为用户的 SecretKey,请登录访问管理控制台进行查看和管理,https://console.cloud.tencent.com/cam/capi
},
})
// 获取预签名URL
presignedURL, err := client.Object.GetPresignedURL(
context.Background(), http.MethodGet, "test123.png", secretID, secretKey, 20*time.Second, nil)
if err != nil {
panic(err)
}
fmt.Println(presignedURL)

}

创建图片存储微服务

上传图片

image-20220407165535854

展示

image-20220407170016379

image-20220325202455885

步骤1:创建blob.proto,并生成blob.pb.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
31
32
33
syntax = "proto3";
package blob.v1;
option go_package = "coolcar/blob/api/gen/v1;blobpb";

message CreateBlobRequest{
string accound_id = 1;
int32 upload_url_time_sec = 2;
}
message CreateBlobResponse{
string id = 1;
string upload_url = 2;
}

message GetBlobRequest{
string id = 1;
}
message GetBlobResponse{
bytes data = 1;
// string mime_type = 2; // text/plain,image/jpg
}
message GetBlobURlRequest{
string id = 1;
int32 time_sec = 2;
}
message GetBlobURLResponse{
string url = 1;
}

service Blobservice{
rpc CreateBlob(CreateBlobRequest) returns (CreateBlobResponse);
rpc GetBlob(GetBlobRequest) returns (GetBlobResponse);
rpc GetBlobURL(GetBlobURlRequest) returns(GetBlobURLResponse);
}

步骤2:创建blob/blob.go,dao/mongo.go,cos/cos.go

  1. blob/blob.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
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
package blob

import (
"context"
blobpb "coolcar/blob/api/gen/v1"
"coolcar/blob/dao"
"coolcar/shared/id"
"io"
"io/ioutil"
"net/http"
"time"

"go.mongodb.org/mongo-driver/mongo"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type Storage interface {
SingURL(c context.Context, method, path string, timeout time.Duration) (string, error)
Get(c context.Context, path string) (io.ReadCloser, error)
}
type Service struct {
Storage Storage
Mongo *dao.Mongo
Logger *zap.Logger
}

func (s *Service) CreateBlob(c context.Context, req *blobpb.CreateBlobRequest) (*blobpb.CreateBlobResponse, error) {
aid := id.AccountID(req.AccoundId)
br, err := s.Mongo.CreateBlob(c, aid)
if err != nil {
s.Logger.Error("cannot create blob:%v", zap.Error(err))
return nil, status.Error(codes.Internal, "")
}
u, err := s.Storage.SingURL(c, http.MethodPut, br.Path, secToDuration(req.UploadUrlTimeSec))
if err != nil {
return nil, status.Errorf(codes.Aborted, "cannot sign url:%v", err)
}
return &blobpb.CreateBlobResponse{
Id: br.ID.Hex(),
UploadUrl: u,
}, nil
}

func (s *Service) GetBlob(c context.Context, req *blobpb.GetBlobRequest) (*blobpb.GetBlobResponse, error) {
br, err := s.getBlobRecord(c, id.BlobID(req.Id))
if err != nil {
return nil, err
}
r, err := s.Storage.Get(c, br.Path)
if r != nil {
defer r.Close()
}
if err != nil {
return nil, status.Errorf(codes.Aborted, "cannot get Storage:%v", err)
}
b, err := ioutil.ReadAll(r)
if err != nil {
return nil, status.Errorf(codes.Aborted, "cannot read from response:%v", err)
}
return &blobpb.GetBlobResponse{
Data: b,
}, nil
}

func (s *Service) GetBlobURL(c context.Context, req *blobpb.GetBlobURlRequest) (*blobpb.GetBlobURLResponse, error) {
br, err := s.getBlobRecord(c, id.BlobID(req.Id))
if err != nil {
return nil, err
}
u, err := s.Storage.SingURL(c, http.MethodGet, br.Path, secToDuration(req.TimeSec))
if err != nil {
return nil, status.Errorf(codes.Aborted, "cannot get Storage:%v", err)
}
return &blobpb.GetBlobURLResponse{
Url: u,
}, nil
}

func (s *Service) getBlobRecord(c context.Context, bid id.BlobID) (*dao.BlobRecord, error) {
br, err := s.Mongo.GetBlob(c, bid)
if err == mongo.ErrNoDocuments {
return nil, status.Error(codes.NotFound, "")
}
if err != nil {
return nil, status.Error(codes.InvalidArgument, err.Error())
}
return br, nil
}

func secToDuration(sec int32) time.Duration {
return time.Duration(sec) * time.Second
}

  1. mongo.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
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
package dao

import (
"context"
"coolcar/shared/id"
mgo "coolcar/shared/mongo"
"coolcar/shared/mongo/objid"
"fmt"

"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)

type Mongo struct {
col *mongo.Collection
}

func NewMongo(db *mongo.Database) *Mongo {
return &Mongo{
col: db.Collection("blob"),
}
}

type BlobRecord struct {
mgo.IDField `bson:"inline"`
accountID string `bson:"accountid"`
Path string `bson:"path"`
}

func (m *Mongo) CreateBlob(c context.Context, aid id.AccountID) (*BlobRecord, error) {
br := &BlobRecord{
accountID: aid.String(),
}
objid := mgo.NewObjID()
br.ID = objid
br.Path = fmt.Sprintf("%s/%s", br.accountID, objid.Hex())
_, err := m.col.InsertOne(c, br)
if err != nil {
return nil, err
}
return br, nil
}

func (m *Mongo) GetBlob(c context.Context, bid id.BlobID) (*BlobRecord, error) {
objid, err := objid.FromID(bid)
if err != nil {
return nil, fmt.Errorf("invalid object id: %v", err)
}
res := m.col.FindOne(c, bson.M{
mgo.IDFieldName: objid,
})
if err := res.Err(); err != nil {
return nil, err
}
var br BlobRecord
err = res.Decode(&br)
if err != nil {
return nil, fmt.Errorf("cannot decode result: %v", err)
}
return &br, nil
}

  1. cos.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
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
package cos

import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"time"

"github.com/tencentyun/cos-go-sdk-v5"
)

type Service struct {
client *cos.Client
secID string
secKey string
}

func NewService(addr, secID, seckey string) (*Service, error) {
u, err := url.Parse(addr)
if err != nil {
return nil, fmt.Errorf("cannot parse addr:%v", err)
}
b := &cos.BaseURL{BucketURL: u}
return &Service{
client: cos.NewClient(b, &http.Client{
Transport: &cos.AuthorizationTransport{
SecretID: secID,
SecretKey: seckey,
},
}),
secID: secID,
secKey: seckey,
}, nil
}

func (s *Service) SingURL(c context.Context, method, path string, timeout time.Duration) (string, error) {
u, err := s.client.Object.GetPresignedURL(c, method, path, s.secID, s.secKey, timeout, nil)
if err != nil {
return "", err
}
return u.String(), nil
}
func (s *Service) Get(c context.Context, path string) (io.ReadCloser, error) {
r, err := s.client.Object.Get(c, path, nil)
var b io.ReadCloser
if r != nil {
b = r.Body
}
if err != nil {
return b, err
}
if r.StatusCode > 400 {
return b, fmt.Errorf("got err response:%+v", err)
}
return b, nil
}

步骤3:注册微服务

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
package main

import (
"context"
blobpb "coolcar/blob/api/gen/v1"
"coolcar/blob/blob"
"coolcar/blob/cos"
"coolcar/blob/dao"
"coolcar/shared/server"
"log"

"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
"go.uber.org/zap"
"google.golang.org/grpc"
)

func main() {
logger, err := zap.NewDevelopment()
if err != nil {
log.Fatalf("cannot create logger : %v", err)
}
mgURI := "mongodb://localhost:27017/coolcar?readPreference=primary&ssl=false"
mongoClient, err := mongo.Connect(context.Background(), options.Client().ApplyURI(mgURI))
if err != nil {
logger.Fatal("cannot connect mongodb: %v", zap.Error(err))
}

db := mongoClient.Database("coolcar")
st, err := cos.NewService(
"https://coolcar-1309863650.cos.ap-nanjing.myqcloud.com",
"AKID2tUpthFWWLuCCxRjgPr2wZv0oLbbKnzu",
"g6F9m8D2arqY1C7mAI9k47sNbaIjtoSp",
)
err = server.RunGRPCServer(&server.GRPCConfig{
Name: "blob",
Addr: ":8083",
Logger: logger,
RegisterFunc: func(s *grpc.Server) {
blobpb.RegisterBlobserviceServer(s, &blob.Service{
Storage: st,
Mongo: dao.NewMongo(db),
Logger: logger,
})
},
})
if err != nil {
logger.Fatal("cannot server: %v", zap.Error(err))
}
}

使用GRPC客户端测试

CreateBlob生成的URL是上传的预签名URL,放在TS文件的用户上传驾驶证图片点击事件方法中,来发送亲求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
onUploadLic() {
console.log('onUploadLic')
wx.chooseImage({
success: res =>{
if (res.tempFilePaths.length >0){
this.setData({
licImgURL:res.tempFilePaths[0]
})
const data = wx.getFileSystemManager().readFileSync(res.tempFilePaths[0])
wx.request({
url: "https://coolcar-1309863650.cos.ap-nanjing.myqcloud.com/account_1/624feb44ccff44db122fec90?q-sign-algorithm=sha1&q-ak=AKID2tUpthFWWLuCCxRjgPr2wZv0oLbbKnzu&q-sign-time=1649404740%3B1649405740&q-key-time=1649404740%3B1649405740&q-header-list=host&q-url-param-list=&q-signature=72e3c545df6fbd447c630f03db35cca53a0d5e28",
data,
method:"PUT",
success:console.log,
fail:console.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
package main

import (
"context"
blobpb "coolcar/blob/api/gen/v1"
"fmt"

"google.golang.org/grpc"
)

func main() {
conn, err := grpc.Dial("localhost:8083", grpc.WithInsecure())
if err != nil {
panic(err)
}
c := blobpb.NewBlobserviceClient(conn)

ctx := context.Background()

// res, err := c.CreateBlob(ctx, &blobpb.CreateBlobRequest{
// AccoundId: "account_1",
// UploadUrlTimeSec: 1000,
// })
// res, err := c.GetBlob(ctx, &blobpb.GetBlobRequest{
// Id: "624feb44ccff44db122fec90",
// })
res, err := c.GetBlobURL(ctx, &blobpb.GetBlobURlRequest{
Id: "624feb44ccff44db122fec90",
TimeSec: 100,
})
if err != nil {
panic(err)
}
fmt.Printf("%+v\n", res)
}

ProfileService

更新rental.proto和rental.yaml代码,并且生成protobuf。

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
syntax = "proto3";
package rental.v1;
option go_package="coolcar/rental/api/gen/v1;rentalpb";

// Trip Service
message Location {
double latitude = 1;
double longitude = 2;
}

message LocationStatus {
Location location = 1;
int32 fee_cent = 2;
double km_driven = 3;
string poi_name = 4;
int64 timestamp_sec = 5;
}

enum TripStatus {
TS_NOT_SPECIFIED = 0;
IN_PROGRESS = 1;
FINISHED = 2;
}

message TripEntity {
string id = 1;
Trip trip = 2;
}

message Trip {
string account_id = 1;
string car_id = 2;
LocationStatus start = 3;
LocationStatus current = 4;
LocationStatus end = 5;
TripStatus status = 6;
string identity_id = 7;
}

message CreateTripRequest {
Location start = 1;
string car_id = 2;
string avatar_url = 3;
}

message GetTripRequest {
string id = 1;
}

message GetTripsRequest {
TripStatus status = 1;
}

message GetTripsResponse {
repeated TripEntity trips = 1;
}

message UpdateTripRequest {
string id = 1;
Location current = 2;
bool end_trip = 3;
}

service TripService {
rpc CreateTrip (CreateTripRequest) returns (TripEntity);
rpc GetTrip (GetTripRequest) returns (Trip);
rpc GetTrips (GetTripsRequest) returns (GetTripsResponse);
rpc UpdateTrip (UpdateTripRequest) returns (Trip);
}

// Profile Service

enum Gender{
G_NOT_SPECIFIED = 0;
FEMALE = 1;
MALE = 2;
}
enum IdentityStatus {
UNSUBMITTED=0;
PENDING =1;
VERIFIED=2;
}
message Profile{
Identity identity = 1;
IdentityStatus identity_status=2;
}

message Identity{
string lic_number=1;
string name = 2;
Gender gender = 3;
int64 birth_date_millis = 4;
}
message GetProfileRequest{}
message ClearProfileRequest{}

message GetProfilePhotoRequest{}
message GetProfilePhotoReponse{
string url = 1;
}
message CreateProfilePhotoRequest{}
message CreateProfilePhotoReponse{
string upload_url = 1;
}

message CompleteProfilePhotoRequest{}

message ClearProfilePhotoRequest{}
message ClearProfilePhotoReponse{}
service ProfileService{
rpc GetProfile (GetProfileRequest) returns (Profile);
rpc SubmitProfile (Identity) returns (Profile);
rpc ClearProfile (ClearProfileRequest) returns (Profile);

rpc GetProfilePhoto(GetProfilePhotoRequest) returns (GetProfilePhotoReponse);
rpc CreateProfilePhoto(CreateProfilePhotoRequest) returns (CreateProfilePhotoReponse);
rpc CompleteProfilePhoto(CompleteProfilePhotoRequest) returns (Identity);
rpc ClearProfilePhoto (ClearProfilePhotoRequest) returns(ClearProfilePhotoReponse);
}
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
type: google.api.Service
config_version: 3

http:
rules:
- selector: rental.v1.TripService.CreateTrip
post: /v1/trip/createTrip
body: "*"
- selector: rental.v1.TripService.GetTrip
get: /v1/trip/{id}
- selector: rental.v1.TripService.GetTrips
get: /v1/trips
- selector: rental.v1.TripService.UpdateTrip
put: /v1/trip/{id}
body: "*"
- selector: rental.v1.ProfileService.GetProfile
get: /v1/profile
- selector: rental.v1.ProfileService.SubmitProfile
post: /v1/profile
body: "*"
- selector: rental.v1.ProfileService.ClearProfile
delete: /v1/profile
- selector: rental.v1.ProfileService.GetProfilePhoto
get: /v1/profile/photo
- selector: rental.v1.ProfileService.CreateProfilePhoto
post: /v1/profile/photo
body: "*"
- selector: rental.v1.ProfileService.CompleteProfilePhoto
post: /v1/profile/photo/complete
body: "*"
- selector: rental.v1.ProfileService.ClearProfilePhoto
delete: /v1/profile/photo

在profile中加入下面代码,实现rpc的方法

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
func (s *Service) GetProfilePhoto(c context.Context, req *rentalpb.GetProfilePhotoRequest) (*rentalpb.GetProfilePhotoReponse, error) {
aid, err := auth.AccountIDFromContext(c)
if err != nil {
return nil, err
}
pr, err := s.Mongo.GetProfile(c, aid)
if err != nil {
code := s.logAndConvertProfileErr(err)
if code == codes.NotFound {
return &rentalpb.GetProfilePhotoReponse{}, nil
}
return nil, status.Error(code, "")
}
if pr.PhotoBlobID == "" {
return nil, status.Error(codes.NotFound, "")
}
br, err := s.BlobClient.GetBlobURL(c, &blobpb.GetBlobURlRequest{
Id: pr.PhotoBlobID,
TimeSec: int32(s.PhotoGetExpire.Seconds()),
})
if err != nil {
s.Logger.Error("cannot get blob", zap.Error(err))
return nil, status.Error(codes.Internal, "")
}
return &rentalpb.GetProfilePhotoReponse{
Url: br.Url,
}, nil
}
func (s *Service) CreateProfilePhoto(c context.Context, req *rentalpb.CreateProfilePhotoRequest) (*rentalpb.CreateProfilePhotoReponse, error) {
aid, err := auth.AccountIDFromContext(c)
if err != nil {
return nil, err
}
br, err := s.BlobClient.CreateBlob(c, &blobpb.CreateBlobRequest{
AccoundId: aid.String(),
UploadUrlTimeSec: int32(s.PhotoUploadExpire.Seconds()),
})
if err != nil {
s.Logger.Error("cannot create blob", zap.Error(err))
return nil, status.Error(codes.Aborted, "")
}
err = s.Mongo.UpdateProfilePhoto(c, aid, id.BlobID(br.Id))
if err != nil {
s.Logger.Error("cannot update blob", zap.Error(err))
return nil, status.Error(codes.Aborted, "")
}
return &rentalpb.CreateProfilePhotoReponse{
UploadUrl: br.UploadUrl,
}, nil
}
func (s *Service) CompleteProfilePhoto(c context.Context, req *rentalpb.CompleteProfilePhotoRequest) (*rentalpb.Identity, error) {
aid, err := auth.AccountIDFromContext(c)
if err != nil {
return nil, err
}

pr, err := s.Mongo.GetProfile(c, aid)
if err != nil {
return nil, status.Error(s.logAndConvertProfileErr(err), "")
}
if pr.PhotoBlobID == "" {
return nil, status.Error(codes.NotFound, "")
}
br, err := s.BlobClient.GetBlob(c, &blobpb.GetBlobRequest{
Id: pr.PhotoBlobID,
})
if err != nil {
return nil, status.Error(codes.NotFound, "")
}
s.Logger.Info("got profile photo", zap.Int("size", len(br.Data)))
return &rentalpb.Identity{
LicNumber: "34088119923324908",
Name: "谭谭",
Gender: rentalpb.Gender_FEMALE,
BirthDateMillis: 23498237948237,
}, nil
}

func (s *Service) ClearProfilePhoto(c context.Context, req *rentalpb.ClearProfilePhotoRequest) (*rentalpb.ClearProfilePhotoReponse, error) {
aid, err := auth.AccountIDFromContext(c)
if err != nil {
return nil, err
}
err = s.Mongo.UpdateProfilePhoto(c, aid, id.BlobID(""))
if err != nil {
s.Logger.Error("cannot clear profile photo", zap.Error(err))
}
return &rentalpb.ClearProfilePhotoReponse{}, nil
}

func (s *Service) logAndConvertProfileErr(err error) codes.Code {
if err != nil {
if err == mongo.ErrNoDocuments {
return codes.NotFound
}
s.Logger.Error("cannot get Profile", zap.Error(err))
return codes.Internal
}
return codes.OK
}

在提交照片的时候,会进行插入操作,数据如下。

image-20220411143237220

这时候,如果在按照以前的操作去更新Profile就会出错,因为查找条件里面有identityStatusField,但是按照上面的数据,没有这个字段,所以会新插入数据,由于accountID是有索引的,只能存在一条,所以会出错。

1
2
3
4
5
6
7
_, err := m.col.UpdateOne(c, bson.M{
accountIdField: aid.String(),
identityStatusField: prevStatus,
}, mgo.Set(bson.M{
accountIdField: aid.String(),
profileField: p,
}), options.Update().SetUpsert(true))

解决办法就是调整查询条件identitystatus为0或者不存在。

1
2
3
4
5
6
7
8
9
10
"$or":[
{
"profile.identitystatus":0
},
{
"profile.identitystatus":{
"$exists":false,
}
}
]

将上面的代码翻译成go,在shared\mongo\mongo.go加入代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func ZeroOrDoesNotExist(field string, zero interface{}) bson.M {
return bson.M{
"$or": []bson.M{
{
field: zero,
},
{
field: bson.M{
"$exists": false,
},
},
},
}
}

修改成下面这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func (m *Mongo) UpdateProfile(c context.Context, aid id.AccountID, prevStatus rentalpb.IdentityStatus, p *rentalpb.Profile) error {
filter := bson.M{
identityStatusField: prevStatus,
}
if prevStatus == rentalpb.IdentityStatus_UNSUBMITTED {
filter = mgo.ZeroOrDoesNotExist(identityStatusField,prevStatus)
}
filter[accountIdField] = aid.String()
_, err := m.col.UpdateOne(c, filter, mgo.Set(bson.M{
accountIdField: aid.String(),
profileField: p,
}), options.Update().SetUpsert(true))
return err
}

profile.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
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
func (s *Service) GetProfilePhoto(c context.Context, req *rentalpb.GetProfilePhotoRequest) (*rentalpb.GetProfilePhotoReponse, error) {
aid, err := auth.AccountIDFromContext(c)
if err != nil {
return nil, err
}
pr, err := s.Mongo.GetProfile(c, aid)
if err != nil {
code := s.logAndConvertProfileErr(err)
if code == codes.NotFound {
return &rentalpb.GetProfilePhotoReponse{}, nil
}
return nil, status.Error(code, "")
}
if pr.PhotoBlobID == "" {
return nil, status.Error(codes.NotFound, "")
}
br, err := s.BlobClient.GetBlobURL(c, &blobpb.GetBlobURlRequest{
Id: pr.PhotoBlobID,
TimeSec: int32(s.PhotoGetExpire.Seconds()),
})
if err != nil {
s.Logger.Error("cannot get blob", zap.Error(err))
return nil, status.Error(codes.Internal, "")
}
return &rentalpb.GetProfilePhotoReponse{
Url: br.Url,
}, nil
}
func (s *Service) CreateProfilePhoto(c context.Context, req *rentalpb.CreateProfilePhotoRequest) (*rentalpb.CreateProfilePhotoReponse, error) {
aid, err := auth.AccountIDFromContext(c)
if err != nil {
return nil, err
}
br, err := s.BlobClient.CreateBlob(c, &blobpb.CreateBlobRequest{
AccoundId: aid.String(),
UploadUrlTimeSec: int32(s.PhotoUploadExpire.Seconds()),
})
if err != nil {
s.Logger.Error("cannot create blob", zap.Error(err))
return nil, status.Error(codes.Aborted, "")
}
err = s.Mongo.UpdateProfilePhoto(c, aid, id.BlobID(br.Id))
if err != nil {
s.Logger.Error("cannot update blob", zap.Error(err))
return nil, status.Error(codes.Aborted, "")
}
return &rentalpb.CreateProfilePhotoReponse{
UploadUrl: br.UploadUrl,
}, nil
}
func (s *Service) CompleteProfilePhoto(c context.Context, req *rentalpb.CompleteProfilePhotoRequest) (*rentalpb.Identity, error) {
aid, err := auth.AccountIDFromContext(c)
if err != nil {
return nil, err
}

pr, err := s.Mongo.GetProfile(c, aid)
if err != nil {
return nil, status.Error(s.logAndConvertProfileErr(err), "")
}
if pr.PhotoBlobID == "" {
return nil, status.Error(codes.NotFound, "")
}
br, err := s.BlobClient.GetBlob(c, &blobpb.GetBlobRequest{
Id: pr.PhotoBlobID,
})
if err != nil {
return nil, status.Error(codes.NotFound, "")
}
s.Logger.Info("got profile photo", zap.Int("size", len(br.Data)))
return &rentalpb.Identity{
LicNumber: "34088119923324908",
Name: "谭谭",
Gender: rentalpb.Gender_FEMALE,
BirthDateMillis: 23498237948237,
}, nil
}

func (s *Service) ClearProfilePhoto(c context.Context, req *rentalpb.ClearProfilePhotoRequest) (*rentalpb.ClearProfilePhotoReponse, error) {
aid, err := auth.AccountIDFromContext(c)
if err != nil {
return nil, err
}
err = s.Mongo.UpdateProfilePhoto(c, aid, id.BlobID(""))
if err != nil {
s.Logger.Error("cannot clear profile photo", zap.Error(err))
}
return &rentalpb.ClearProfilePhotoReponse{}, nil
}

func (s *Service) logAndConvertProfileErr(err error) codes.Code {
if err != nil {
if err == mongo.ErrNoDocuments {
return codes.NotFound
}
s.Logger.Error("cannot get Profile", zap.Error(err))
return codes.Internal
}
return codes.OK
}

测试

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
func TestProfilePhotoLifecycle(t *testing.T) {
c := auth.ContestWithAccontId(context.Background(), id.AccountID("account1"))
s := newService(c, t)
s.BlobClient = &blobClient{
idForCreate: "blob1",
}
cases := []struct {
name string
op func() (string, error)
wantRUL string
wantErrCode codes.Code
}{
{
name: "create_blob",
op: func() (string, error) {
r, err := s.CreateProfilePhoto(c, &rentalpb.CreateProfilePhotoRequest{})
if err != nil {
return "", err
}
return r.UploadUrl, nil
},
wantRUL: "upload_url for blob1",
},
{
name: "complete_photo_upload",
op: func() (string, error) {
_, err := s.CompleteProfilePhoto(c, &rentalpb.CompleteProfilePhotoRequest{})
return "", err
},
},
{
name: "get_photo_url",
op: func() (string, error) {
r, err := s.GetProfilePhoto(c, &rentalpb.GetProfilePhotoRequest{})
if err != nil {
return "", err
}
return r.Url, nil
},
wantRUL: "get_url for blob1",
},
{
name: "clear_photo",
op: func() (string, error) {
_, err := s.ClearProfilePhoto(c, &rentalpb.ClearProfilePhotoRequest{})

return "", err
},
},
{
name: "get_photo_after_clear",
op: func() (string, error) {
r, err := s.GetProfilePhoto(c, &rentalpb.GetProfilePhotoRequest{})
if err != nil {
return "", err
}
return r.Url, nil
},
wantErrCode: codes.NotFound,
},
}

for _, cc := range cases {
got, err := cc.op()
code := codes.OK
if err != nil {
if s, ok := status.FromError(err); ok {
code = s.Code()
} else {
t.Errorf("operation failed:%v", err)
}
}
if code != cc.wantErrCode {
t.Errorf("%s: want error %d, got %d", cc.name, cc.wantErrCode, code)
}
if got != cc.wantRUL {
t.Errorf("%s: want %q, got %q", cc.name, cc.wantRUL, got)
}
}
}

type blobClient struct {
idForCreate string
}

func (b *blobClient) CreateBlob(c context.Context, req *blobpb.CreateBlobRequest, opts ...grpc.CallOption) (*blobpb.CreateBlobResponse, error) {
return &blobpb.CreateBlobResponse{
Id: b.idForCreate,
UploadUrl: "upload_url for " + b.idForCreate,
}, nil
}
func (b *blobClient) GetBlob(c context.Context, req *blobpb.GetBlobRequest, opts ...grpc.CallOption) (*blobpb.GetBlobResponse, error) {
return &blobpb.GetBlobResponse{}, nil
}
func (b *blobClient) GetBlobURL(c context.Context, req *blobpb.GetBlobURlRequest, opts ...grpc.CallOption) (*blobpb.GetBlobURLResponse, error) {
return &blobpb.GetBlobURLResponse{
Url: "get_url for " + req.Id,
}, nil
}