未验证 提交 e0ccbf01 编写于 作者: P Phodal Huang

feat: add first version arch

上级 6501a230
......@@ -17,7 +17,6 @@
.idea/
html/
tequila
*.gch
......
......@@ -33,7 +33,8 @@ Todo:
- [ ] Testable?
- [ ] Test badsmell
- [ ] Test bad smell list [https://testsmells.github.io/pages/testsmells.html]
- Arch
## Usage
install
......@@ -444,6 +445,10 @@ go get github.com/onsi/gomega
License
---
Arch based on [Tequila](https://github.com/newlee/tequila)
Git Analysis inspired by [Code Maat](https://github.com/adamtornhill/code-maat)
[![Phodal's Idea](http://brand.phodal.com/shields/idea-small.svg)](http://ideas.phodal.com/)
@ 2019 A [Phodal Huang](https://www.phodal.com)'s [Idea](http://github.com/phodal/ideas). This code is distributed under the MPL license. See `LICENSE` in this directory.
......@@ -4,8 +4,10 @@ import (
"fmt"
"github.com/phodal/coca/cmd/cmd_util"
"github.com/phodal/coca/config"
"github.com/phodal/coca/core/adapter"
"github.com/phodal/coca/core/domain/arch"
"github.com/spf13/cobra"
"strings"
)
type ArchCmdConfig struct {
......@@ -21,12 +23,27 @@ var archCmd = &cobra.Command{
Short: "generate arch",
Long: ``,
Run: func(cmd *cobra.Command, args []string) {
identifiers = adapter.LoadIdentify(apiCmdConfig.DependencePath)
identifiersMap = adapter.BuildIdentifierMap(identifiers)
parsedDeps := cmd_util.GetDepsFromJson(archCmdConfig.DependencePath)
archApp := arch.NewArchApp()
dotContent := archApp.Analysis(parsedDeps)
dotContent := archApp.Analysis(parsedDeps, identifiersMap)
fmt.Println(dotContent)
//ConvertToSvg(dotContent)
ignores := strings.Split("", ",")
var nodeFilter = func(key string) bool {
for _, f := range ignores {
if key == f {
return true
}
}
return false
}
dotContent.ToDot("coca_reporter/arch.dot", ".", nodeFilter)
cmd_util.ConvertToSvg("arch")
},
}
......
package arch
import "github.com/phodal/coca/core/models"
import (
"github.com/phodal/coca/core/domain/arch/tequila"
"github.com/phodal/coca/core/models"
)
type ArchApp struct {
}
......@@ -9,6 +12,27 @@ func NewArchApp() ArchApp {
return *&ArchApp{}
}
func (a ArchApp) Analysis(deps []models.JClassNode) string {
return ""
func (a ArchApp) Analysis(deps []models.JClassNode, identifiersMap map[string]models.JIdentifier) tequila.FullGraph {
fullGraph := tequila.FullGraph{
NodeList: make(map[string]string),
RelationList: make(map[string]*tequila.Relation),
}
for _, clz := range deps {
src := clz.Package + "." + clz.Class
fullGraph.NodeList[src] = src
for _, call := range clz.MethodCalls {
dst := call.Package + "." + call.Class
if _, ok := identifiersMap[dst]; ok {
relation := &tequila.Relation{
From: dst,
To: src,
Style: "\"solid\"",
}
fullGraph.RelationList[relation.From+"->"+relation.To] = relation }
}
}
return *&fullGraph
}
MIT License
Copyright (c) 2017 Li Xin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
\ No newline at end of file
package tequila
import (
"bufio"
"fmt"
"github.com/awalterschulze/gographviz"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
)
type Relation struct {
From string
To string
Style string
}
type FullGraph struct {
NodeList map[string]string
RelationList map[string]*Relation
}
type Fan struct {
Name string
FanIn int
FanOut int
}
func (f *FullGraph) FindCrossRef(merge func(string) string) []string {
mergedRelationMap := make(map[string]string)
result := make([]string, 0)
for key := range f.RelationList {
relation := f.RelationList[key]
mergedFrom := merge(relation.From)
mergedTo := merge(relation.To)
if mergedFrom == mergedTo {
continue
}
if _, ok := mergedRelationMap[mergedTo+mergedFrom]; ok {
result = append(result, mergedFrom+" <-> "+mergedTo)
}
mergedRelationMap[mergedFrom+mergedTo] = ""
}
return result
}
func (f *FullGraph) MergeHeaderFile(merge func(string) string) *FullGraph {
result := &FullGraph{
NodeList: make(map[string]string),
RelationList: make(map[string]*Relation),
}
nodes := make(map[string]string)
for key := range f.NodeList {
mergedKey := merge(key)
nodes[key] = mergedKey
result.NodeList[mergedKey] = mergedKey
}
for key := range f.RelationList {
relation := f.RelationList[key]
mergedFrom := merge(relation.From)
mergedTo := merge(relation.To)
if mergedFrom == mergedTo {
continue
}
mergedRelation := &Relation{
From: mergedFrom,
To: mergedTo,
Style: "\"solid\"",
}
result.RelationList[mergedRelation.From+mergedRelation.To] = mergedRelation
}
return result
}
func (f *FullGraph) EntryPoints(merge func(string) string) []string {
mergedGraph := f.MergeHeaderFile(merge)
fromMap := make(map[string]bool)
toMap := make(map[string]bool)
for key := range mergedGraph.RelationList {
relation := mergedGraph.RelationList[key]
if relation.From == "main" {
continue
}
fromMap[relation.From] = true
toMap[relation.To] = true
}
result := make([]string, 0)
for key := range fromMap {
if _, ok := toMap[key]; !ok {
result = append(result, key)
}
}
return result
}
func (f *FullGraph) SortedByFan(merge func(string) string) []*Fan {
mergedGraph := f.MergeHeaderFile(merge)
result := make([]*Fan, len(mergedGraph.NodeList))
index := 0
fanMap := make(map[string]*Fan)
for key := range mergedGraph.NodeList {
fan := &Fan{Name: key}
result[index] = fan
fanMap[key] = fan
index++
}
for key := range mergedGraph.RelationList {
relation := mergedGraph.RelationList[key]
fanMap[relation.From].FanOut++
fanMap[relation.To].FanIn++
}
sort.Slice(result, func(i, j int) bool {
return (result[i].FanIn + result[i].FanOut) > (result[j].FanIn + result[j].FanOut)
})
return result
}
var fullGraph *FullGraph
func parseRelation(edge *gographviz.Edge, nodes map[string]string) {
if _, ok := nodes[edge.Src]; ok {
if _, ok := nodes[edge.Dst]; ok {
dst := nodes[edge.Dst]
src := nodes[edge.Src]
dst = strings.ToLower(dst)
src = strings.ToLower(src)
relation := &Relation{
From: dst,
To: src,
Style: "\"solid\"",
}
fullGraph.RelationList[relation.From+"->"+relation.To] = relation
}
}
}
func filterDirectory(fullMethodName string) bool {
if strings.Contains(fullMethodName, "_test") {
return true
}
if strings.Contains(fullMethodName, "Test") {
return true
}
if strings.Contains(fullMethodName, "/Library/") {
return true
}
return false
}
func parseDotFile(codeDotfile string) {
fbuf, _ := ioutil.ReadFile(codeDotfile)
parseFromBuffer(fbuf)
}
func parseFromBuffer(fbuf []byte) {
g, err := gographviz.Read(fbuf)
if err != nil {
fmt.Println(string(fbuf))
}
nodes := make(map[string]string)
for _, node := range g.Nodes.Nodes {
fullMethodName := strings.Replace(node.Attrs["label"], "\"", "", 2)
if strings.Contains(fullMethodName, " ") {
tmp := strings.Split(fullMethodName, " ")
fullMethodName = tmp[len(tmp)-1]
}
if filterDirectory(fullMethodName) {
continue
}
methodName := formatMethodName(fullMethodName)
fullGraph.NodeList[methodName] = methodName
nodes[node.Name] = methodName
}
for key := range g.Edges.DstToSrcs {
for edgesKey := range g.Edges.DstToSrcs[key] {
for _, edge := range g.Edges.DstToSrcs[key][edgesKey] {
parseRelation(edge, nodes)
}
}
}
}
func formatMethodName(fullMethodName string) string {
methodName := strings.Replace(fullMethodName, "\\l", "", -1)
methodName = strings.Replace(methodName, "src/", "", -1)
methodName = strings.Replace(methodName, "include/", "", -1)
methodName = strings.ToLower(methodName)
return methodName
}
func codeDotFiles(codeDir string, fileFilter func(string) bool) []string {
codeDotFiles := make([]string, 0)
filepath.Walk(codeDir, func(path string, fi os.FileInfo, err error) error {
if strings.HasSuffix(path, ".dot") {
if fileFilter(path) {
//return nil
if strings.Contains(path, "_test_") {
return nil
}
codeDotFiles = append(codeDotFiles, path)
}
}
return nil
})
return codeDotFiles
}
func ParseInclude(codeDir string) *FullGraph {
fullGraph = &FullGraph{
NodeList: make(map[string]string),
RelationList: make(map[string]*Relation),
}
codeDotFiles := codeDotFiles(codeDir, func(path string) bool {
return strings.HasSuffix(path, "_dep__incl.dot")
})
for _, codeDotfile := range codeDotFiles {
parseDotFile(codeDotfile)
}
return fullGraph
}
func (fullGraph *FullGraph) ToDot(fileName string, split string, filter func(string) bool) {
graph := gographviz.NewGraph()
graph.SetName("G")
nodeIndex := 1
layerIndex := 1
nodes := make(map[string]string)
layerMap := make(map[string][]string)
for nodeKey := range fullGraph.NodeList {
if filter(nodeKey) {
continue
}
tmp := strings.Split(nodeKey, split)
packageName := tmp[0]
if packageName == nodeKey {
packageName = "main"
}
if len(tmp) > 2 {
packageName = strings.Join(tmp[0:len(tmp)-1], split)
}
if _, ok := layerMap[packageName]; !ok {
layerMap[packageName] = make([]string, 0)
}
layerMap[packageName] = append(layerMap[packageName], nodeKey)
}
for layer := range layerMap {
layerAttr := make(map[string]string)
layerAttr["label"] = "\"" + layer + "\""
layerName := "cluster" + strconv.Itoa(layerIndex)
graph.AddSubGraph("G", layerName, layerAttr)
layerIndex++
for _, node := range layerMap[layer] {
attrs := make(map[string]string)
fileName := strings.Replace(node, layer+split, "", -1)
attrs["label"] = "\"" + fileName + "\""
attrs["shape"] = "box"
graph.AddNode(layerName, "node"+strconv.Itoa(nodeIndex), attrs)
nodes[node] = "node" + strconv.Itoa(nodeIndex)
nodeIndex++
}
}
cross := make(map[string]bool) // mapping from strings to ints
for key := range fullGraph.RelationList {
relation := fullGraph.RelationList[key]
if nodes[relation.From] != "" && nodes[relation.To] != "" {
fromNode := nodes[relation.From]
toNode := nodes[relation.To]
cross[fromNode+toNode] = true
attrs := make(map[string]string)
attrs["style"] = relation.Style
graph.AddEdge(fromNode, toNode, true, attrs)
}
}
f, _ := os.Create(fileName)
w := bufio.NewWriter(f)
w.WriteString("di" + graph.String())
w.Flush()
}
var Foo = func() string {
return ""
}
func (fullGraph *FullGraph) ToDataSet(fileName string, split string, filter func(string) bool) {
nodes := make(map[string]string)
for nodeKey := range fullGraph.NodeList {
if filter(nodeKey) {
continue
}
nodes[nodeKey] = nodeKey
}
relMap := make(map[string][]string)
for key := range fullGraph.RelationList {
relation := fullGraph.RelationList[key]
if nodes[relation.From] == "" && nodes[relation.To] != "" {
if _, ok := relMap[relation.From]; !ok {
relMap[relation.From] = make([]string, 0)
}
relMap[relation.From] = append(relMap[relation.From], relation.To)
}
}
for key := range relMap {
tos := relMap[key]
fmt.Print("['" + strings.Join(tos, "','") + "'],")
}
}
......@@ -4,18 +4,18 @@ go 1.13
require (
github.com/antlr/antlr4 v0.0.0-20191031194250-3fcb6da1f690
github.com/awalterschulze/gographviz v0.0.0-20190522210029-fa59802746ab
github.com/boyter/scc v2.10.1+incompatible
github.com/dbaggerman/cuba v0.3.2 // indirect
github.com/iancoleman/strcase v0.0.0-20191112232945-16388991a334
github.com/minio/blake2b-simd v0.0.0-20160723061019-3f5f724cb5b1 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20160105113617-38717d0a108c // indirect
github.com/olekukonko/tablewriter v0.0.4
github.com/onsi/ginkgo v1.10.3
github.com/onsi/ginkgo v1.10.3 // indirect
github.com/onsi/gomega v1.7.1
github.com/pkg/profile v1.3.0
github.com/sabhiram/go-gitignore v0.0.0-20180611051255-d3107576ba94
github.com/spf13/cobra v0.0.5
golang.org/x/exp v0.0.0-20191224044220-1fea468a75e9 // indirect
golang.org/x/tools v0.0.0-20191226230302-065ed046f11a // indirect
gonum.org/v1/gonum v0.6.2
)
......@@ -5,6 +5,8 @@ github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3
github.com/antlr/antlr4 v0.0.0-20191031194250-3fcb6da1f690 h1:ZxuHyCRVyeoXhu/PK93BvZP66NoNIsSZzCykW5MezKc=
github.com/antlr/antlr4 v0.0.0-20191031194250-3fcb6da1f690/go.mod h1:T7PbCXFs94rrTttyxjbyT5+/1V8T2TYDejxUfHJjw1Y=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/awalterschulze/gographviz v0.0.0-20190522210029-fa59802746ab h1:+cdNqtOJWjvepyhxy23G7z7vmpYCoC65AP0nqi1f53s=
github.com/awalterschulze/gographviz v0.0.0-20190522210029-fa59802746ab/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs=
github.com/boyter/scc v2.10.1+incompatible h1:0bg2TmmduZjvoSqz0dfF3W3jfibgEFv2XOsKItTjarU=
github.com/boyter/scc v2.10.1+incompatible/go.mod h1:VB5w4e0dahmIiKnpZ7LRh/sjauoY0BmCWjIzZcShNY0=
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
......@@ -73,7 +75,6 @@ github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2 h1:y102fOLFqhV41b+4GPiJoa0k/x+pJcEi2/HB1Y5T6fU=
......@@ -86,7 +87,6 @@ golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMx
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
......@@ -94,12 +94,14 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwL
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A=
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
......@@ -107,13 +109,11 @@ golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e h1:Io7mpb+aUAGF0MKxbyQ7HQl1VgB+cL6ZJZUFaFNqVV4=
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191226230302-065ed046f11a h1:dzKmLFHGw+XKWQ9gr1JrQ0lVhDehzY+5Am/i7wxZlMs=
golang.org/x/tools v0.0.0-20191226230302-065ed046f11a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo=
gonum.org/v1/gonum v0.6.2 h1:4r+yNT0+8SWcOkXP+63H2zQbN+USnC73cjGUxnDF94Q=
gonum.org/v1/gonum v0.6.2/go.mod h1:9mxDZsDKxgMAuccQkewq682L+0eCu4dCN2yonUJTCLU=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0 h1:OE9mWmgKkjJyEmDAAtGMPjXu+YNeGvK9VTSHY6+Qihc=
gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw=
gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册