正在显示
19 个修改的文件
包含
508 行增加
和
72 行删除
document/terms/schemas/im.yaml
0 → 100644
| 1 | +version: v1 | ||
| 2 | +kind: Schema | ||
| 3 | +metadata: | ||
| 4 | + name: im | ||
| 5 | + description: 冗余附加数据 | ||
| 6 | + attributes: | ||
| 7 | + - name: accid | ||
| 8 | + description: 网易云信ID | ||
| 9 | + type: | ||
| 10 | + primitive: string | ||
| 11 | + - name: imToken | ||
| 12 | + description: 网易云信Token | ||
| 13 | + type: | ||
| 14 | + primitive: string | ||
| 15 | + - name: csAccountId | ||
| 16 | + description: 系统分配客服ID | ||
| 17 | + type: | ||
| 18 | + primitive: string |
| @@ -14,6 +14,10 @@ metadata: | @@ -14,6 +14,10 @@ metadata: | ||
| 14 | description: 手机认证 | 14 | description: 手机认证 |
| 15 | type: | 15 | type: |
| 16 | schemal: phoneAuth | 16 | schemal: phoneAuth |
| 17 | + - name: im | ||
| 18 | + description: IM信息 | ||
| 19 | + type: | ||
| 20 | + schemal: im | ||
| 17 | - ref: createAt | 21 | - ref: createAt |
| 18 | required: true | 22 | required: true |
| 19 | - ref: updateAt | 23 | - ref: updateAt |
| @@ -31,9 +31,9 @@ metadata: | @@ -31,9 +31,9 @@ metadata: | ||
| 31 | type: | 31 | type: |
| 32 | array: int64 | 32 | array: int64 |
| 33 | - name: collectedMenus | 33 | - name: collectedMenus |
| 34 | - description: 收藏的菜单(工作台) | 34 | + description: 收藏的菜单(工作台)(菜单编码列表) |
| 35 | type: | 35 | type: |
| 36 | - array: menu | 36 | + array: string |
| 37 | - name: cooperationInfo | 37 | - name: cooperationInfo |
| 38 | description: 共创信息 (共创用户有效) | 38 | description: 共创信息 (共创用户有效) |
| 39 | type: | 39 | type: |
pkg/domain/im.go
0 → 100644
| 1 | +package domainService | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "fmt" | ||
| 5 | + pgTransaction "github.com/linmadan/egglib-go/transaction/pg" | ||
| 6 | + "gitlab.fjmaimaimai.com/mmm-go-pp/terms/pkg/domain" | ||
| 7 | + "gitlab.fjmaimaimai.com/mmm-go-pp/terms/pkg/infrastructure/im" | ||
| 8 | +) | ||
| 9 | + | ||
| 10 | +type PgImService struct { | ||
| 11 | + transactionContext *pgTransaction.TransactionContext | ||
| 12 | +} | ||
| 13 | + | ||
| 14 | +func (s *PgImService) InitOrUpdateUserIMInfo(userId int64, name string) (imInfo *domain.Im, err error) { | ||
| 15 | + var ( | ||
| 16 | + //ImInfoRepository, _ = factory.CreateImInfoRepository(ctx) | ||
| 17 | + checkImRequest *im.CheckImRequest = &im.CheckImRequest{} | ||
| 18 | + IsCreated = false | ||
| 19 | + checkImResponse *im.CheckImResponse | ||
| 20 | + ) | ||
| 21 | + var errFind error | ||
| 22 | + //imInfo, errFind = ImInfoRepository.FindOne(map[string]interface{}{"user_id": userId}) | ||
| 23 | + // 异常 | ||
| 24 | + //if errFind != nil && errFind != domain.QueryNoRow { | ||
| 25 | + // err = errFind | ||
| 26 | + // return | ||
| 27 | + //} | ||
| 28 | + // 不存在 | ||
| 29 | + //if errFind == domain.QueryNoRow { | ||
| 30 | + // imInfo = &domain.Im{ | ||
| 31 | + // UserId: userId, | ||
| 32 | + // CreateTime: time.Now(), | ||
| 33 | + // } | ||
| 34 | + //} | ||
| 35 | + // 已存在 | ||
| 36 | + if errFind == nil && imInfo != nil { | ||
| 37 | + IsCreated = true | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + if len(imInfo.Accid) == 0 { | ||
| 41 | + //id, _ := utils.NewSnowflakeId() | ||
| 42 | + //imInfo.ImId = fmt.Sprintf("%v", id) | ||
| 43 | + } | ||
| 44 | + checkImRequest = &im.CheckImRequest{ | ||
| 45 | + UserId: userId, | ||
| 46 | + ImId: imInfo.Accid, | ||
| 47 | + Uname: name, | ||
| 48 | + CustomerImId: fmt.Sprintf("%v", imInfo.CsAccountId), | ||
| 49 | + IsCreated: IsCreated, | ||
| 50 | + } | ||
| 51 | + if checkImResponse, err = CheckIm(checkImRequest); err != nil { | ||
| 52 | + return | ||
| 53 | + } | ||
| 54 | + if len(imInfo.CsAccountId) == 0 { | ||
| 55 | + imInfo.CsAccountId = getRandomCustomerAccount(userId) | ||
| 56 | + } | ||
| 57 | + imInfo.ImToken = checkImResponse.ImToken | ||
| 58 | + //if _, err = ImInfoRepository.Save(imInfo); err != nil { | ||
| 59 | + // return | ||
| 60 | + //} | ||
| 61 | + return | ||
| 62 | +} | ||
| 63 | + | ||
| 64 | +// 检查ImToken | ||
| 65 | +func CheckIm(request *im.CheckImRequest) (rsp *im.CheckImResponse, err error) { | ||
| 66 | + var () | ||
| 67 | + rsp = &im.CheckImResponse{} | ||
| 68 | + if !request.IsCreated { | ||
| 69 | + if err = imCreate(request, rsp); err != nil { | ||
| 70 | + return | ||
| 71 | + } | ||
| 72 | + } else { | ||
| 73 | + if err = imUpdate(request, rsp); err != nil { | ||
| 74 | + return | ||
| 75 | + } | ||
| 76 | + } | ||
| 77 | + if err = imRefreshToken(request, rsp); err != nil { | ||
| 78 | + return | ||
| 79 | + } | ||
| 80 | + return | ||
| 81 | +} | ||
| 82 | + | ||
| 83 | +//create | ||
| 84 | +func imCreate(request *im.CheckImRequest, rsp *im.CheckImResponse) (err error) { | ||
| 85 | + var ( | ||
| 86 | + param im.UserCreate = im.UserCreate{ | ||
| 87 | + Accid: request.ImId, | ||
| 88 | + Name: request.Uname, | ||
| 89 | + Icon: request.Icon, | ||
| 90 | + } | ||
| 91 | + out *im.UserTokenResult | ||
| 92 | + ) | ||
| 93 | + if out, err = im.CallCreate(param); err != nil { | ||
| 94 | + return | ||
| 95 | + } | ||
| 96 | + if out.Code != 200 || (out.Info.Accid != request.ImId) { | ||
| 97 | + return im.ErrorFailCall | ||
| 98 | + } | ||
| 99 | + rsp.ImToken = out.Info.Token | ||
| 100 | + return | ||
| 101 | +} | ||
| 102 | + | ||
| 103 | +//update user info | ||
| 104 | +func imUpdate(request *im.CheckImRequest, rsp *im.CheckImResponse) (err error) { | ||
| 105 | + var ( | ||
| 106 | + param im.UserUpdate = im.UserUpdate{ | ||
| 107 | + Accid: request.ImId, | ||
| 108 | + Name: request.Uname, | ||
| 109 | + Icon: request.Icon, | ||
| 110 | + } | ||
| 111 | + out *im.BaseResp | ||
| 112 | + ) | ||
| 113 | + if out, err = im.CallUpdate(param); err != nil { | ||
| 114 | + return | ||
| 115 | + } | ||
| 116 | + if out.Code != 200 { | ||
| 117 | + return im.ErrorFailCall | ||
| 118 | + } | ||
| 119 | + return | ||
| 120 | +} | ||
| 121 | + | ||
| 122 | +//refresh token | ||
| 123 | +func imRefreshToken(request *im.CheckImRequest, rsp *im.CheckImResponse) (err error) { | ||
| 124 | + var ( | ||
| 125 | + param im.UserRefreshToken = im.UserRefreshToken{ | ||
| 126 | + Accid: request.ImId, | ||
| 127 | + } | ||
| 128 | + out *im.UserTokenResult | ||
| 129 | + ) | ||
| 130 | + if out, err = im.CallRefreshToken(param); err != nil { | ||
| 131 | + return | ||
| 132 | + } | ||
| 133 | + if out.Code != 200 || (out.Info.Accid != request.ImId) { | ||
| 134 | + return im.ErrorFailCall | ||
| 135 | + } | ||
| 136 | + rsp.ImToken = out.Info.Token | ||
| 137 | + return | ||
| 138 | +} | ||
| 139 | + | ||
| 140 | +// 获取客服id | ||
| 141 | +func getRandomCustomerAccount(userId int64) (acid string) { | ||
| 142 | + //ImCustomerServiceRepository, _ := factory.CreateImCustomerServiceRepository(ctx) | ||
| 143 | + //total, customers, err := ImCustomerServiceRepository.Find(map[string]interface{}{"sortById": domain.ASC}) | ||
| 144 | + //if err != nil { | ||
| 145 | + // log.Error(err) | ||
| 146 | + // return 0 | ||
| 147 | + //} | ||
| 148 | + //if total == 0 { | ||
| 149 | + // return 0 | ||
| 150 | + //} | ||
| 151 | + //index := userId % total | ||
| 152 | + //if int(index) < len(customers) { | ||
| 153 | + // acid, _ = strconv.ParseInt(customers[index].ImId, 10, 64) | ||
| 154 | + // return | ||
| 155 | + //} | ||
| 156 | + //acid, _ = strconv.ParseInt(customers[0].ImId, 10, 64) | ||
| 157 | + return | ||
| 158 | +} |
pkg/infrastructure/im/im.go
0 → 100644
| 1 | +package im | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "encoding/json" | ||
| 5 | +) | ||
| 6 | + | ||
| 7 | +//func init() { | ||
| 8 | +// InitImClient(constant.IM_SERVICE_ADDRESS, constant.IM_APP_KEY, constant.IM_APP_SECRET) | ||
| 9 | +//} | ||
| 10 | + | ||
| 11 | +type RequestParam interface { | ||
| 12 | + Format() map[string]string | ||
| 13 | + GetPath() string | ||
| 14 | + Valid() error | ||
| 15 | +} | ||
| 16 | + | ||
| 17 | +//接口 | ||
| 18 | +func CallCreate(v UserCreate) (*UserTokenResult, error) { | ||
| 19 | + var result UserTokenResult | ||
| 20 | + btData, err := DefaultImClient.Call(v) | ||
| 21 | + if err != nil { | ||
| 22 | + return nil, err | ||
| 23 | + } | ||
| 24 | + err = json.Unmarshal(btData, &result) | ||
| 25 | + if err != nil { | ||
| 26 | + return nil, err | ||
| 27 | + } | ||
| 28 | + return &result, nil | ||
| 29 | +} | ||
| 30 | +func CallRefreshToken(v UserRefreshToken) (*UserTokenResult, error) { | ||
| 31 | + var result UserTokenResult | ||
| 32 | + btData, err := DefaultImClient.Call(v) | ||
| 33 | + if err != nil { | ||
| 34 | + return nil, err | ||
| 35 | + } | ||
| 36 | + err = json.Unmarshal(btData, &result) | ||
| 37 | + if err != nil { | ||
| 38 | + return nil, err | ||
| 39 | + } | ||
| 40 | + return &result, nil | ||
| 41 | +} | ||
| 42 | +func CallUpdate(v UserUpdate) (*BaseResp, error) { | ||
| 43 | + var result BaseResp | ||
| 44 | + btData, err := DefaultImClient.Call(v) | ||
| 45 | + if err != nil { | ||
| 46 | + return nil, err | ||
| 47 | + } | ||
| 48 | + err = json.Unmarshal(btData, &result) | ||
| 49 | + if err != nil { | ||
| 50 | + return nil, err | ||
| 51 | + } | ||
| 52 | + return &result, nil | ||
| 53 | +} | ||
| 54 | + | ||
| 55 | +/*CheckIm */ | ||
| 56 | +type CheckImRequest struct { | ||
| 57 | + UserId int64 | ||
| 58 | + ImId string | ||
| 59 | + Uname string | ||
| 60 | + Icon string | ||
| 61 | + CustomerImId string | ||
| 62 | + IsCreated bool | ||
| 63 | +} | ||
| 64 | +type CheckImResponse struct { | ||
| 65 | + ImToken string //net im token | ||
| 66 | + CsAccount int64 //客服id | ||
| 67 | +} |
pkg/infrastructure/im/im_test.go
0 → 100644
| 1 | +package im | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "testing" | ||
| 5 | +) | ||
| 6 | + | ||
| 7 | +const ( | ||
| 8 | + IM_SERVICE_ADDRESS = "https://api.netease.im/nimserver" | ||
| 9 | + IM_APP_KEY = "be7c0639c10e6a69f86ce3b4fa8dc8ec" //"ebf3ae278ee1b346773b99be5080f6a9" | ||
| 10 | + IM_APP_SECRET = "9c5b60346613" //"67ea92e1ea45" | ||
| 11 | +) | ||
| 12 | + | ||
| 13 | +func TestCallCreate(t *testing.T) { | ||
| 14 | + InitImClient(IM_SERVICE_ADDRESS, IM_APP_KEY, IM_APP_SECRET) | ||
| 15 | + token, err := CallCreate(UserCreate{Accid: "1"}) | ||
| 16 | + if err != nil { | ||
| 17 | + t.Fatal(err) | ||
| 18 | + } | ||
| 19 | + if token == nil { | ||
| 20 | + t.Fatal("token is nil") | ||
| 21 | + } | ||
| 22 | + t.Log(token.Code, token.Info) | ||
| 23 | +} | ||
| 24 | + | ||
| 25 | +func TestCallRefreshToken(t *testing.T) { | ||
| 26 | + InitImClient(IM_SERVICE_ADDRESS, IM_APP_KEY, IM_APP_SECRET) | ||
| 27 | + token, err := CallRefreshToken(UserRefreshToken{Accid: "1"}) | ||
| 28 | + if err != nil { | ||
| 29 | + t.Fatal(err) | ||
| 30 | + } | ||
| 31 | + if token == nil { | ||
| 32 | + t.Fatal("token is nil") | ||
| 33 | + } | ||
| 34 | + t.Log(token.Code, token.Info) | ||
| 35 | +} | ||
| 36 | + | ||
| 37 | +func TestCallUpdate(t *testing.T) { | ||
| 38 | + InitImClient(IM_SERVICE_ADDRESS, IM_APP_KEY, IM_APP_SECRET) | ||
| 39 | + token, err := CallUpdate(UserUpdate{Accid: "1", Name: "tip tok"}) | ||
| 40 | + if err != nil { | ||
| 41 | + t.Fatal(err) | ||
| 42 | + } | ||
| 43 | + if token == nil { | ||
| 44 | + t.Fatal("token is nil") | ||
| 45 | + } | ||
| 46 | + t.Log(token.Code) | ||
| 47 | +} |
pkg/infrastructure/im/netease.go
0 → 100644
| 1 | +package im | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "crypto/sha1" | ||
| 5 | + "encoding/hex" | ||
| 6 | + "fmt" | ||
| 7 | + "io/ioutil" | ||
| 8 | + "math/rand" | ||
| 9 | + "net/http" | ||
| 10 | + "net/url" | ||
| 11 | + "strconv" | ||
| 12 | + "strings" | ||
| 13 | + "time" | ||
| 14 | +) | ||
| 15 | + | ||
| 16 | +var DefaultImClient Client | ||
| 17 | + | ||
| 18 | +var ErrorFailCall = fmt.Errorf(" imclient call failed") | ||
| 19 | + | ||
| 20 | +func InitImClient(baseUrl, appKey, appSecret string) { | ||
| 21 | + DefaultImClient = Client{ | ||
| 22 | + baseUrl: baseUrl, | ||
| 23 | + appKey: appKey, | ||
| 24 | + appSecret: appSecret, | ||
| 25 | + } | ||
| 26 | +} | ||
| 27 | + | ||
| 28 | +type Client struct { | ||
| 29 | + baseUrl string | ||
| 30 | + appKey string | ||
| 31 | + appSecret string | ||
| 32 | +} | ||
| 33 | + | ||
| 34 | +func (i Client) Call(param RequestParam) ([]byte, error) { | ||
| 35 | + return i.httpDo(param.GetPath(), param.Format()) | ||
| 36 | +} | ||
| 37 | +func (i Client) buildHeader() http.Header { | ||
| 38 | + var h = http.Header{} | ||
| 39 | + curTime := strconv.FormatInt(time.Now().Unix(), 10) | ||
| 40 | + nonce := strconv.FormatInt(time.Now().Unix()+rand.Int63n(5000), 10) | ||
| 41 | + checkSum := buildCheckSum(i.appSecret, nonce, curTime) | ||
| 42 | + h.Set("Content-Type", "application/x-www-form-urlencoded") | ||
| 43 | + h.Set("AppKey", i.appKey) | ||
| 44 | + h.Set("Nonce", nonce) | ||
| 45 | + h.Set("CurTime", curTime) | ||
| 46 | + h.Set("CheckSum", checkSum) | ||
| 47 | + return h | ||
| 48 | +} | ||
| 49 | +func (i Client) httpDo(path string, posts map[string]string) ([]byte, error) { | ||
| 50 | + client := http.Client{ | ||
| 51 | + Timeout: 5 * time.Second, //请求超时时间5秒 | ||
| 52 | + } | ||
| 53 | + reqURL := i.baseUrl + path | ||
| 54 | + params := url.Values{} | ||
| 55 | + for k, v := range posts { | ||
| 56 | + params.Add(k, v) | ||
| 57 | + } | ||
| 58 | + req, err := http.NewRequest("POST", reqURL, strings.NewReader(params.Encode())) | ||
| 59 | + if err != nil { | ||
| 60 | + return nil, err | ||
| 61 | + } | ||
| 62 | + req.Header = i.buildHeader() | ||
| 63 | + resp, err := client.Do(req) | ||
| 64 | + if err != nil { | ||
| 65 | + return nil, err | ||
| 66 | + } | ||
| 67 | + defer resp.Body.Close() | ||
| 68 | + | ||
| 69 | + body, err := ioutil.ReadAll(resp.Body) | ||
| 70 | + if err != nil { | ||
| 71 | + return nil, err | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + return body, nil | ||
| 75 | +} | ||
| 76 | + | ||
| 77 | +func buildCheckSum(appSecret string, nonce string, curTime string) string { | ||
| 78 | + str := []byte(appSecret + nonce + curTime) | ||
| 79 | + sh := sha1.New() | ||
| 80 | + sh.Write(str) | ||
| 81 | + result := hex.EncodeToString(sh.Sum(nil)) | ||
| 82 | + return strings.ToLower(result) | ||
| 83 | +} |
pkg/infrastructure/im/netease_request.go
0 → 100644
| 1 | +package im | ||
| 2 | + | ||
| 3 | +import ( | ||
| 4 | + "fmt" | ||
| 5 | +) | ||
| 6 | + | ||
| 7 | +var ( | ||
| 8 | + _ RequestParam = UserCreate{} | ||
| 9 | + _ RequestParam = UserUpdate{} | ||
| 10 | + _ RequestParam = UserRefreshToken{} | ||
| 11 | +) | ||
| 12 | + | ||
| 13 | +type BaseResp struct { | ||
| 14 | + Code int `json:"code"` | ||
| 15 | +} | ||
| 16 | + | ||
| 17 | +// TokenInfo 云通信Token | ||
| 18 | +type TokenInfo struct { | ||
| 19 | + Token string `json:"token"` | ||
| 20 | + Accid string `json:"accid"` | ||
| 21 | + Name string `json:"name"` | ||
| 22 | +} | ||
| 23 | +type UserTokenResult struct { | ||
| 24 | + BaseResp | ||
| 25 | + Info TokenInfo `json:"info"` | ||
| 26 | +} | ||
| 27 | + | ||
| 28 | +// 创建网易云通信ID | ||
| 29 | +type UserCreate struct { | ||
| 30 | + Accid string //网易云通信ID,最大长度32字符 | ||
| 31 | + Name string //ID昵称,最大长度64字符。 | ||
| 32 | + Props string //json属性,开发者可选填,最大长度1024字符 | ||
| 33 | + Icon string //ID头像URL,开发者可选填,最大长度1024字符 | ||
| 34 | + /** | ||
| 35 | + 云通信ID可以指定登录token值,最大长度128字符, | ||
| 36 | + 并更新,如果未指定,会自动生成token,并在 | ||
| 37 | + 创建成功后返回 | ||
| 38 | + **/ | ||
| 39 | + Token string | ||
| 40 | + Sign string //签名 | ||
| 41 | + Email string | ||
| 42 | + Birth string | ||
| 43 | + Mobile string | ||
| 44 | + Gender int //0未知,1男,2女 | ||
| 45 | + Ex string //扩展字段 | ||
| 46 | +} | ||
| 47 | + | ||
| 48 | +func (p UserCreate) Format() map[string]string { | ||
| 49 | + return map[string]string{ | ||
| 50 | + "accid": p.Accid, | ||
| 51 | + "name": p.Name, | ||
| 52 | + "props": p.Props, | ||
| 53 | + "icon": p.Icon, | ||
| 54 | + "token": p.Token, | ||
| 55 | + "sign": p.Sign, | ||
| 56 | + "email": p.Email, | ||
| 57 | + "birth": p.Birth, | ||
| 58 | + "mobile": p.Mobile, | ||
| 59 | + "gender": fmt.Sprintf("%d", p.Gender), | ||
| 60 | + "ex": p.Ex, | ||
| 61 | + } | ||
| 62 | +} | ||
| 63 | +func (p UserCreate) GetPath() string { | ||
| 64 | + return "/user/create.action" | ||
| 65 | +} | ||
| 66 | +func (p UserCreate) Valid() error { | ||
| 67 | + return nil | ||
| 68 | +} | ||
| 69 | + | ||
| 70 | +// 重置网易云通信token | ||
| 71 | +type UserRefreshToken struct { | ||
| 72 | + Accid string //网易云通信ID,最大长度32字符,必须保证一个 APP内唯一 | ||
| 73 | +} | ||
| 74 | + | ||
| 75 | +func (p UserRefreshToken) Format() map[string]string { | ||
| 76 | + return map[string]string{ | ||
| 77 | + "accid": p.Accid, | ||
| 78 | + } | ||
| 79 | +} | ||
| 80 | +func (p UserRefreshToken) GetPath() string { | ||
| 81 | + return "/user/refreshToken.action" | ||
| 82 | +} | ||
| 83 | +func (p UserRefreshToken) Valid() error { | ||
| 84 | + return nil | ||
| 85 | +} | ||
| 86 | + | ||
| 87 | +// 更新网易云通信token | ||
| 88 | +type UserUpdate struct { | ||
| 89 | + Accid string | ||
| 90 | + Name string //这边网易云要有昵称以手机号码为昵称 | ||
| 91 | + Icon string //icon默认头像 | ||
| 92 | + Sign string //签名 | ||
| 93 | + Email string | ||
| 94 | + Birth string | ||
| 95 | + Mobile string | ||
| 96 | + Gender int //0未知,1男,2女 | ||
| 97 | + Ex string //扩展字段 | ||
| 98 | +} | ||
| 99 | + | ||
| 100 | +func (u UserUpdate) Format() map[string]string { | ||
| 101 | + return map[string]string{ | ||
| 102 | + "accid": u.Accid, | ||
| 103 | + "name": u.Name, | ||
| 104 | + "icon": u.Icon, | ||
| 105 | + "sign": u.Sign, | ||
| 106 | + "email": u.Email, | ||
| 107 | + "birth": u.Birth, | ||
| 108 | + "mobile": u.Mobile, | ||
| 109 | + "gender": fmt.Sprintf("%d", u.Gender), | ||
| 110 | + "ex": u.Ex, | ||
| 111 | + } | ||
| 112 | +} | ||
| 113 | +func (u UserUpdate) GetPath() string { | ||
| 114 | + return "/user/refreshToken.action" | ||
| 115 | +} | ||
| 116 | +func (u UserUpdate) Valid() error { | ||
| 117 | + return nil | ||
| 118 | +} |
-
请 注册 或 登录 后发表评论