V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
dbfox
V2EX  ›  程序员

分享下我写不需要太严谨的项目的代码风格

  •  
  •   dbfox · 2015-11-19 11:41:03 +08:00 · 4280 次点击
    这是一个创建于 3078 天前的主题,其中的信息可能已经有所发展或是发生改变。

    public static Common.DB.ResultList GetList(Common.DB.NVCollection queryParams)
    {

    //string pageString = queryParams["page"] as string ?? "1";
            //string psString = queryParams["pagesize"] as string ?? string.Empty;
            //string cidString = queryParams["cid"] as string ?? string.Empty;
    
            string sort = queryParams["sort"] as string ?? string.Empty;
            string cate = queryParams["cate"] as string ?? string.Empty;
            string type = queryParams["type"] as string ?? string.Empty;
            string ch = queryParams["ch"] as string ?? string.Empty;
            string kind = queryParams["kind"] as string ?? string.Empty;
    
            string andwhere = "1=1";
            string rootKey = null;
            switch (type)
            {
                case "appsoft":
                    rootKey = "iphones";
                    break;
                case "appgame":
                    rootKey = "iphoneg";
                    break;
                case "azsoft":
                    rootKey = "soft";
                    break;
                case "azgame":
                    rootKey = "game";
                    break;
            }
    
            var pnvc = new Common.DB.NVCollection();
    
            if (rootKey != null)
            {
                var pcate = DBCache.SoftCategoryCache.Get(rootKey);
                if (pcate != null)
                {
                    andwhere += " and charIndex(@path,categoryPath)>0 ";
                    pnvc["path"] = "/" + pcate.ID + "/";
                }
            }
    
            if (ch == "az")
            {
                andwhere += " and (charIndex(@path1,categoryPath)>0 or charIndex(@path2,categoryPath)>0)";
                pnvc["path1"] = "/" + DBCache.SoftCategoryCache.Get("game").ID + "/";
                pnvc["path2"] = "/" + DBCache.SoftCategoryCache.Get("soft").ID + "/";
            }
            else if (ch == "app")
            {
                andwhere += " and (charIndex(@path1,categoryPath)>0 or charIndex(@path2,categoryPath)>0)";
                pnvc["path1"] = "/" + DBCache.SoftCategoryCache.Get("appgame").ID + "/";
                pnvc["path2"] = "/" + DBCache.SoftCategoryCache.Get("appsoft").ID + "/";
            }
    
    
            if (!string.IsNullOrEmpty(cate) && !string.IsNullOrEmpty(rootKey))
            {
                var cateEnt = DBCache.SoftCategoryCache.Get(rootKey, cate);
    
                if (cateEnt != null)
                {
                    andwhere += " and categoryid=@cid ";
                    pnvc["cid"] = cateEnt.ID;
                }
            }
    
    
    
    
            int cid = (queryParams["cid"] as int?) ?? 0;
            if (cid > 0)
            {
                var cateEnt = DBCache.SoftCategoryCache.Get(cid);
    
                if (cateEnt != null)
                {
                    andwhere += " and categoryid=@cid ";
                    pnvc["cid"] = cateEnt.ID;
                }
            }
    
            string orderby = " id desc";
            switch (sort)
            {
                case "new":
                    orderby = " id desc";
                    break;
                case "hot":
                    orderby = " viewTimes desc,id desc";
                    break;
                case "rank":
                    orderby = " downWeekTimes desc,id desc";
    
                    break;
                default:
                    break;
            }
    
            switch (kind)
            {
                case "number":
                    andwhere += " and number>0";
                    orderby = " number desc,id desc ";
                    break;
                case "hprec":
                    andwhere += " and recommend=1 and homepage=1 and number=0 ";
                    orderby = " viewtimes desc,id desc ";
                    break;
                case "recnothp":
                    andwhere += " and recommend=1 and homepage=0 and number=0 ";
                    orderby = " viewtimes desc,id desc ";
                    break;
                case "rec":
                    andwhere += " and recommend=1 ";
                    break;
    
                case "all":
    
                    break;
    
                case "normal":
                default:
                    andwhere += " and recommend=0 and homepage=0 and number=0 ";
                    //orderby = " id desc ";
                    break;
            }
    
    
    
    
    
            int page = (queryParams["page"] as int?) ?? 1;
            if (page <= 0)
            {
                page = 1;
            }
    
            int pagesize = (queryParams["pagesize"] as int?) ?? 10;
    
            if (pagesize <= 0)
            {
                pagesize = 10;
            }
    
            Common.DB.ResultList result = new Common.DB.ResultList(pagesize);
    
            var dbh = Common.DB.Factory.Default;
    
            //List<Common.DB.NVCollection> list = new List<Common.DB.NVCollection>(pagesize);
    
            var query = Common.DB.Factory.DefaultPagerQuery;
            query.AbsolutePage = page;
            query.Fields = "id,name,version,viewtimes,categoryid";
            query.PageSize = pagesize;
            query.Sort = orderby;
            query.Table = "soft";
            query.Where = andwhere;
    
            string csql = query.GetCountQueryString();
            string qsql = query.GetQueryString();
    
            int rc = dbh.ExecuteScalar<int>(csql, pnvc);
            var ls = dbh.GetDataList(qsql, pnvc);
    
            int pc = Convert.ToInt32(Math.Ceiling((decimal)rc / (decimal)pagesize));
    
    
            result.Page = page;
            result.PageCount = pc;
            result.RecordCount = rc;
            result.PageSize = pagesize;
    
            for (int i = 0; i < ls.Count; i++)
            {
                var o = ls[i];
    
                var ent = new Common.DB.NVCollection();
                var entcate = DBCache.SoftCategoryCache.Get((int)o["categoryid"]);
    
                ent["id"] = o["id"];
                ent["name"] = o["name"];
                ent["version"] = o["version"];               
                ent["cid"] = o["categoryid"];
                ent["cpath"] = Services.PathService.GetListPath(entcate);
                ent["cname"] = entcate.Name;
                ent["path"] = Services.PathService.GetPath(entcate, (int)o["id"]);
                ent["dtimes"] = o["viewtimes"];
    
                result.Add(ent);
            }
    
            return result;
        }
    
    第 1 条附言  ·  2015-11-19 18:32:07 +08:00

    另外一种写法

    public static List<Result> GetList(int page,int pagesize,SortEnum sort)
    {

    }

    另外一种写法

    //查询参数
    public class QueryP
    {
    public int Page{get;set;}
    public int PageSize{get;set;}
    public SortEnum Sort{get;set;}

    }

    public static List<Result> GetList(QueryP query)
    {

    }

    第 2 条附言  ·  2015-11-20 09:36:54 +08:00
    KeyObjectCollection kvc = new KeyObjectCollection();
    kvc["page"] = int.Parse(接收到传输过来的 Page); //这里是已经经过转换的,所以不用担心注入
    
    
    kvc["追加日期参数"] = DateTime.Parse(接收参数);
    
    
    public static KeyObjectResultList GetList(KeyObjectCollection query) {
    
    
        int page = query["page"]as int; //把 object 类型拆箱,为 int 类型
    
        DateTime 日期参数 = kvc["追加日期参数"] as DateTime; //日期类型
    
    
    
    
    
    }
    
    第 3 条附言  ·  2015-11-20 09:43:13 +08:00
    这样写,我能想到的缺点:

    ide 无法自动提示参数和类型,需要手工去写比较详细的注释和类型
    第 4 条附言  ·  2015-11-20 14:03:39 +08:00
    我代码可维护性非常好,不服来辩
    44 条回复    2015-11-23 13:40:16 +08:00
    dong3580
        1
    dong3580  
       2015-11-19 11:48:06 +08:00
    C#?如果是 web 端,看起来一堆可以放到前端判断了,没必要浪费服务器资源,
    你这种写法。。。
    看不下去了,也不封个方法。。。
    wizardforcel
        2
    wizardforcel  
       2015-11-19 11:53:54 +08:00 via Android
    能把一大堆 case 换成 dictionary 嘛
    jarlyyn
        3
    jarlyyn  
       2015-11-19 12:01:42 +08:00
    这有什么分享的价值么……

    不是应该写一个类判断用户输入,一个类处理 sql 么。

    控制器里负责其他的么……

    数据库调一个字段名 /排序要遍历并修改所有相关的不相关的控制器结构。

    要杀了接受的程序员的节奏。
    vivisidea
        4
    vivisidea  
       2015-11-19 12:02:49 +08:00
    我总觉得手动拼接 sql 的方式不仅容易出问题,而且难以维护
    jarlyyn
        5
    jarlyyn  
       2015-11-19 12:02:57 +08:00
    @dong3580

    前端判断, ORZ ,收下我的膝盖吧……
    liujiangbei
        6
    liujiangbei  
       2015-11-19 12:54:39 +08:00
    直接被 fire
    lifanxi
        7
    lifanxi  
       2015-11-19 13:01:28 +08:00
    @dong3580 也许他前端判断了呢?但是不管前端有没有判断,服务器端总应该判断一次。前端判断是性能优化需求,但是服务器判断是正确性需求。
    dong3580
        8
    dong3580  
       2015-11-19 13:18:24 +08:00
    @lifanxi
    也是也是,哈哈,拼接 sql 语句,不能忍,
    dbfox
        9
    dbfox  
    OP
       2015-11-19 18:28:09 +08:00
    @jarlyyn 太繁琐,说了是不严谨,按照 传统规格去写,写死我了
    dbfox
        10
    dbfox  
    OP
       2015-11-19 18:28:56 +08:00
    @dong3580
    @vivisidea

    很讨厌用框架,尤其是注重性能的地方
    dbfox
        11
    dbfox  
    OP
       2015-11-19 18:33:39 +08:00
    dong3580
        12
    dong3580  
       2015-11-19 18:40:06 +08:00
    @dbfox
    是的。 sql 语句的话如果自己抓东西,还是直接用 sql 语句快点。
    但是自己写也可以加参数嘛,你这样拼接代码就有问题,
    jarlyyn
        13
    jarlyyn  
       2015-11-19 18:40:22 +08:00
    @dbfox

    和繁琐严谨有什么关系呢?

    只不过是把代码适当的区分开而已。能多些几行代码呢?

    我觉得,说到底是没被这样的代码坑过而已,又或者不知道这样的代码坑在哪罢了。
    bramblex
        14
    bramblex  
       2015-11-19 18:50:53 +08:00   ❤️ 2
    @dbfox

    盲目相信自己写的东西“性能”比框架好是一种盲目且愚蠢的做法,框架本就是平衡多项考虑的工程产物。

    当然啦,如果你要自己造玩具当然可以乱来。我造玩具的时候乱来不是一点半点。
    longaiwp
        15
    longaiwp  
       2015-11-19 20:33:00 +08:00
    @dbfox 这个写的我觉得真是要疯了啊
    wizardforcel
        16
    wizardforcel  
       2015-11-19 20:54:51 +08:00
    @dong3580 后端不判断就等着被渗透吧

    @dbfox 过度追求执行效率而忽视开发效率是不对的 性能优化的一条重要原则就是 优化带来的性能提升至少要能抵消代码可读性的下降

    你以前用了一个数组存东西 后来发现用 set 会更好一点 这叫优化

    你以前用了至少执行几十次的一个循环 后来把循环展开了 这不叫优化 这叫作死
    JamesRuan
        17
    JamesRuan  
       2015-11-19 21:28:20 +08:00
    函数太长,差评!
    lawrencexu
        18
    lawrencexu  
       2015-11-19 22:22:00 +08:00
    C#和 Java 程序员被黑就是因为楼主这样的太多了。
    dbfox
        19
    dbfox  
    OP
       2015-11-20 09:30:10 +08:00
    @lawrencexu
    @jarlyyn
    @lawrencexu
    @longaiwp
    @wizardforcel


    可能我表达的不够清楚还是不要管我内部怎么实现了,主要是在 方法的参数上


    如果是以前,我会这样定义方法:

    public static Common.DB.ResultList GetList(int page,int pagesize,string sort,string type,....)


    调用的时候,大家应该都知道,要进行繁琐的类型转换 int.Parse()

    需求一旦一改,增加一个参数或者改变一个参数,函数就被破坏掉了,或者要追加一个方法,同时要改变方法内部的实现


    public static Common.DB.ResultList GetList(int page,int pagesize,int cid,string sort,string type,....)




    后来我想可以增加一个参数类


    public class QueryParams
    {
    public int Page{get;set;}
    public int PageSize{get;set;}
    public string Sort{get;set;}
    public int Cid{get;set;}
    }



    这时候方法就变成这样:
    public static Common.DB.ResultList GetList(queryParams Query);

    增加参数,减少参数,都可以通过修改 QueryParams 的属性 和方法内部实现,而不用破坏方法对外的改变


    再后来,我发现这样还是比较繁琐,写得太严谨,反而很费时

    干脆把参数变更为 和 PHP 相似的 key-object 弱类型,返回值也是弱类型,这样开发效率高了很多倍

    在外部传入参数的时候
    KeyObjectCollection kvc = new KeyObjectCollection();
    kvc["page"] = int.Parse(接收到传输过来的 Page);//这里是已经经过转换的,所以不用担心注入

    public static KeyObjectResultList GetList(KeyObjectCollection query){

    int page = query["page"] as int; //把 object 类型拆箱,为 int 类型

    }

    我发现这样更灵活,更方便,这里我本来不想讨论我 sql 拼接,我之前也用过一些框架 entity framework 等,觉得真的不好用,而且我懒得深入学习这些东西,性能和开发效率还有项目的灵活性,都不太容易掌控,所以我不愿意去用,我当然能体会到一些 orm 的好处,但是带来的一些诟病也不少,不如直接我什么都不想,直接拼 sql 好了,诸位大神,也可以说说,你们怎么写的,顺便学习学习。

    我追求的东西其实也很简单:就是 - “简”,
    只有把项目尽可能做到非常简单,依赖的东西非常少,项目才省心。
    就像象棋,很少的规则,可以玩得很有意思。
    dbfox
        20
    dbfox  
    OP
       2015-11-20 09:57:53 +08:00
    @bramblex 不仅仅是性能,还有框架无法实现的细节,框架也不够灵活,必要的时候还是 tmd 要用 sql

    生成出来一堆 code ,在追加字段,更改字段,删除某字段的时候,烦的一笔,发布项目更烦, entity framework 至少是这样
    jarlyyn
        21
    jarlyyn  
       2015-11-20 10:02:22 +08:00
    @dbfox

    1.这和类型有什么关系?这是你的代码没有合适的函数话好不?正常代码不是主流程表逻辑,业务丢函数么?谁能一眼看清你这代码做了什么?你让以后接收修改你这程序的甚至是半年后的自己情何以堪?

    2.不要手工拼接 sql 和 orm 有什么关系? orm 是把数据模型化吧?对应手工拼接 Sql 的不是 prepara 么……

    所以说你需要好好研究下框架的代码,不是说框架代码有多优秀,而是看看成熟的代码怎么去处理这些坑。
    dbfox
        22
    dbfox  
    OP
       2015-11-20 10:12:27 +08:00
    @jarlyyn

    1 、 我这里 应当算数据访问层,不太刻意追求分层,复杂的地方 会有封装,但绝不可以追求分层。可能我们不在一个领域

    2 、 prepara .net 下有这玩意儿?

    框架我一点都不喜欢,只想掌握最根本的东西,万变不离其宗
    jarlyyn
        23
    jarlyyn  
       2015-11-20 10:27:11 +08:00
    @dbfox

    1.这个是所有代码最基本的地方,和你什么层没关系。和怎么写出一个可以维护的代码有关。

    2.prepara 是数据的事情。 mysql 可以,我查过 sqlserver 也可以。这个和.net 有什么关系?随便搜索一下

    https://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlcommand.prepare(v=vs.110).aspx

    其他的不多说了。祝您写代码快乐。
    dbfox
        24
    dbfox  
    OP
       2015-11-20 13:48:49 +08:00
    @jarlyyn

    听得我听腻歪,找张图片回复你吧:




    SqlCommand.Prepare
    对重复要执行的语句,使用这个方法可以提高执行效率。
    跟我拼接 sql 有毛关系???
    jarlyyn
        25
    jarlyyn  
       2015-11-20 14:16:33 +08:00
    @dbfox

    好的,我犯贱。

    v2ex 是不能删帖子的。

    只怕您以后在 V2EX 找工作的话会暴露水平。

    祝一切顺风。
    ewBuyVmLZMZE
        26
    ewBuyVmLZMZE  
       2015-11-20 14:28:50 +08:00
    这代码看得我尿崩。
    fwrq41251
        27
    fwrq41251  
       2015-11-20 14:33:38 +08:00   ❤️ 1
    静态语言当成动态语言来用.缺点 LZ 也说了:"ide 无法自动提示参数和类型,需要手工去写比较详细的注释和类型".规模小的时候这样开发是会比较快速.可维护性好就不一定了,比如你的 queryParams 里的某个 field 的类型或者名字变化了,而用到这个 queryParams 的地方又很多,如果你定义了它的类型和名称用 IDE 可以很轻松的重构所有需要修改的地方,而像现在这样写,维护起来就比较痛苦了.这也是像 java 这样一种啰嗦的语言的优势所在.
    leassy
        28
    leassy  
       2015-11-20 14:34:51 +08:00
    楼主写 C#啊,看着好亲切
    xujif
        29
    xujif  
       2015-11-20 14:40:24 +08:00
    第一个阶段,拼接
    第二个阶段,参数封装
    第三个阶段, orm
    第四个阶段, orm+复杂查询,比如 mybatis 之类封装语句
    之后,基本就变成仓储模式,不关心数据放在哪里
    楼主既然是 c#,不用 linq 这等神器吗,就算不用 ef , linq to sql 也有不少 provider 可以用吧。
    msg7086
        30
    msg7086  
       2015-11-20 15:22:54 +08:00
    说了半天其实就想说这个 KeyValuePair 的 Collection 拿来传参数?
    别闹了,看看这个吧: https://msdn.microsoft.com/en-us/library/dd264739.aspx

    至于拼接 SQL 我就不多吐槽了,大家已经吐了很多了。
    不用关系代数而用字符串,这本来就是坏的编程风格了,都不用去看效率高低。

    总之我只希望初学者不要被这样的代码风格影响就好。
    iamppz
        31
    iamppz  
       2015-11-22 13:23:41 +08:00
    写得非常好,请继续坚持







    坚持几年你就明白了
    dbfox
        32
    dbfox  
    OP
       2015-11-22 20:19:30 +08:00
    @jarlyyn 不要激动,讨论个代码而已

    只是不明白你说的要怎么写,这样写代码之前,我也写过很多其它方式
    dbfox
        33
    dbfox  
    OP
       2015-11-22 20:20:31 +08:00
    @msg7086
    不拼接 sql 你们怎么写?
    msg7086
        34
    msg7086  
       2015-11-23 00:07:16 +08:00
    @dbfox 用关系代数啊。我相信这么多年了.net 应该有很多基于关系代数的库了吧。
    msg7086
        35
    msg7086  
       2015-11-23 08:57:15 +08:00   ❤️ 1
    用 Ruby 给你写了一个简单的例子,基于 Rails 的。

    https://gist.github.com/msg7086/60e806bcccc00a49ecf4

    我不敢说我的代码是可维护性好的代码,但是至少比你的代码要可维护得多。

    比如说 Ruby 这边建议每个方法不应该超过 10 行,否则可维护性就会大大降低。我这边主函数将近 40 行,很明显的维护性就差很多。很多逻辑还可以抽离出来,使得结构更清晰。至于你这 180 行的函数我就不多说了,离讨论维护性这件事都差得很远。

    读你的代码,首先第一个问题就是不知道这个函数到底接受哪些参数。(也就是你说的,连 IDE 都猜不透你函数到底接受什么东西。)这就意味着,一,新人要用你代码的时候,完全不知道该怎么用;二,你自己参数如果写错一个字母,你根本发现不了。等出了错你就慢慢 debug 吧。

    其次是单函数结构。上面也说了,每个方法不应该超过 10 行。我们退一步讲,每个函数不应该超过 50 行好了。很多逻辑结构都可以抽出来做成单独的方法。看你这个 get_list ,应该是 Controller 方法吧,但是里面的很多逻辑是 Model 逻辑,应该放进 Model 类里。这样把函数拆开以后,还有一个好处就是做测试更方便了。
    我不知道你的代码有多少自动化测试,不过我这边做开发,除非是做了就扔的项目,否则全都有做自动化测试覆盖。测试的代码量至少应该要达到项目代码量的一半甚至更多(通常是和项目代码量相同的级别)。这样项目在发布出去以后, bug 要少得多,哪怕用多一倍的时间去写测试,最后也会比不写测试要节约更多更多的时间。

    最后就是拼接字符串,上面也说了不应该用字符串而应该用关系代数。用关系代数的话,可能会用到 ORM ,不过不用 ORM 应该也是可以做的,不知道 C#有没有可用的类库。但是用 C#不用 LINQ ,简直就是浪费了 C#在做 Web 上的最大优势之一了。(函数式, lambda ,延迟求值,匿名类,哪个不是超级好用的东西?)

    你看我用了关系代数以后,根本不需要写一堆 AND ,不需要分 anywhere 和 orderby ,不需要考虑 WHERE 和 ORDER 的执行顺序,甚至连分页都不用我考虑了——因为有自动的分页插件,会自己往关系代数里注入合适的 LIMIT 子句。代码量轻松就掉了一半,开发速度更快了,而且出错的几率也下降了。

    如上面很多人所说,程序员必然要经过很多个阶段,去反思自己的问题,才能向前走。你现在到达了封装参数发请求的阶段,自然觉得现在的方法是很不错。然而等你将来有机会接触了更好的方法,或者发现现有方法的各种坑了以后,才会知道更好的方法到底好在哪里。

    把心态放平一些,然后多学习多看看吧。
    dbfox
        36
    dbfox  
    OP
       2015-11-23 09:26:15 +08:00
    @jarlyyn

    我查了下,你说的是 参数化查询的意思,我的 sql 是参数化查询,已经封装到 dbh
    只是和 mysql 有区别,你们用的 ?,我这里用的 @ ,所以我这里不存在注入的问题
    andwhere += " and categoryid=@cid ";







    著作权归作者所有。
    商业转载请联系作者获得授权,非商业转载请注明出处。
    作者:余天升
    链接: http://www.zhihu.com/question/22953267/answer/23192081
    来源:知乎

    $stmt = $mysqli->prepare("DELETE FROM planet WHERE name = ?");
    $stmt->bind_param('s', "earth");
    $stmt->execute();



    @msg7086

    我真的不明白拼接 sql 怎么了?不拼接怎么写?
    dbfox
        37
    dbfox  
    OP
       2015-11-23 09:31:57 +08:00
    @msg7086

    是的,要把参数写的很详细

    关系代数,是什么,我得去看看,.net 中似乎没这个东西,

    linq 还是算了吧,从 linq to sql 到 linq to entity 都用过,不好用,深有体会
    msg7086
        38
    msg7086  
       2015-11-23 09:36:58 +08:00
    LINQ 就是关系代数的一种实现。

    PS: 我知道 LINQ to SQL 很难用。归根结底是因为 C#是一个比较静态的语言,不像 Ruby 那样可以玩各种奇技淫巧。
    dbfox
        39
    dbfox  
    OP
       2015-11-23 09:38:05 +08:00
    @xujif

    linq ef linq to sql 这些东西都不好用,不用它也是有原因的
    xujif
        40
    xujif  
       2015-11-23 11:19:26 +08:00
    @dbfox 虽然不用 c#好多年,建议能用强类型表示的还是用强类型,包括 linq ,能在编译期发现的问题就不要留到运行期。
    repus911
        41
    repus911  
       2015-11-23 11:44:01 +08:00
    好在当年没有继续干 C#...

    可维护性很好?
    单元测试?抽象化或者函数拆分?再不济注释?

    你要是说实现一个底层实现 别人用用不知道你怎么实现也就算了 可你发出来并讨论可维护性...再接再厉
    wizardforcel
        42
    wizardforcel  
       2015-11-23 12:49:41 +08:00 via Android
    @dbfox 你要说 linq (以及其他 orm )不好配置这个我倒是同意

    要说他不好用 返回的对象不比 resultset 好用的多嘛。
    dbfox
        43
    dbfox  
    OP
       2015-11-23 13:38:47 +08:00
    @wizardforcel

    不是很喜欢生成的那一堆东西,频繁增删改字段的时候,你就知道有多么烦了
    dbfox
        44
    dbfox  
    OP
       2015-11-23 13:40:16 +08:00
    @xujif
    @repus911

    开发方向不一样,某些类型的项目是不需要写的太严禁的,我就是把 C# 当 PHP 用了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   1179 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 18:18 · PVG 02:18 · LAX 11:18 · JFK 14:18
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.