SQLServer实现跨库跨服务器访问的方法

mikel阅读(1072)

来源: SQLServer实现跨库跨服务器访问的方法

前言

我们经常会遇到一个数据库要访问另一个数据库,或者一台服务器要访问另一台服务器里面的数据库。 那么这个如何实现的呢? 相信看完这篇文章你就懂了!

同一台服务器跨库访问实现

1. 首先创建两个数据库CrossLibraryTable1,CrossLibraryTable2

–创建CrossLibraryTable1脚本:

  1. use master –打开master数据库,一般的创建语句都在master中执行.
  2. go
  3. if exists (select * from sysdatabases where name=’CrossLibraryTable1′)
  4. drop database CrossLibraryTable1 /*检查有没有这个数据库,如果有就删除它。*/
  5. go
  6. create database CrossLibraryTable1
  7. on primary
  8. (
  9. name=’CrossLibraryTable1_data’, ———— 养成好习惯,数据文件加_data
  10. filename=’F:\代码存放\数据库\CrossLibraryTable1_data.mdf’, ———— 一定要是.mdf的文件,代表主数据文件
  11. size=5mb, –默认数据库大小
  12. maxsize=100mb, –最大容量
  13. filegrowth=1mb –增长量
  14. )
  15. log on
  16. (
  17. name=’CrossLibraryTable1_log’, ———— 养成好习惯,日志文件加_log
  18. filename=’F:\代码存放\数据库\CrossLibraryTable1_log.ldf’, ———— 一定要是.ldf的文件,代表日志文件
  19. size=1mb, –默认数据库大小
  20. filegrowth=10% –增长量
  21. )

–创建CrossLibraryTable2脚本:

  1. use master –打开master数据库,一般的创建语句都在master中执行.
  2. go
  3. if exists (select * from sysdatabases where name=’CrossLibraryTable2′)
  4. drop database CrossLibraryTable2 /*检查有没有这个数据库,如果有就删除它。*/
  5. go
  6. create database CrossLibraryTable2
  7. on primary
  8. (
  9. name=’CrossLibraryTable2_data’, ———— 养成好习惯,数据文件加_data
  10. filename=’F:\代码存放\数据库\CrossLibraryTable2_data.mdf’, ———— 一定要是.mdf的文件,代表主数据文件
  11. size=5mb, –默认数据库大小
  12. maxsize=100mb, –最大容量
  13. filegrowth=1mb –增长量
  14. )
  15. log on
  16. (
  17. name=’CrossLibraryTable2_log’, ———— 养成好习惯,日志文件加_log
  18. filename=’F:\代码存放\数据库\CrossLibraryTable2_log.ldf’, ———— 一定要是.ldf的文件,代表日志文件
  19. size=1mb, –默认数据库大小
  20. filegrowth=10% –增长量
  21. )

然后,执行完脚本后,刷新一下就可以看到刚刚创建的数据库了:

2.接下来在两个数据库里面分别创建一个CrossTest1和一个CrossTest2表用于跨库查询

–创建CrossTest1脚本:

  1. use CrossLibraryTable1
  2. create table CrossTest1(
  3. Id int primary key identity,
  4. Name nvarchar(20)
  5. )

–创建CrossTest2脚本:

  1. use CrossLibraryTable2
  2. create table CrossTest2(
  3. Id int primary key identity,
  4. Name nvarchar(20)
  5. )

表创建好后,我们再添加几条数据进去:

  1. use CrossLibraryTable1
  2. insert into CrossTest1 values(‘跨库1测试数据1’)
  3. insert into CrossTest1 values(‘跨库1测试数据2’)
  4. use CrossLibraryTable2
  5. insert into CrossTest2 values(‘跨库2测试数据1’)
  6. insert into CrossTest2 values(‘跨库2测试数据2’)

切换到CrossLibraryTable1下面查询CrossLibraryTable2的数据可以看到报如下错误

修正代码:

PS:像上面那样是可以进行查询,然而在项目中使用上面的代码格式就会引发一些问题,那什么问题呢?比如另一个数据库的名称改变了,我们就需要把所有用到这个的地方都得改掉,这样就很麻烦,那么有什么解决方案么,使得改一处就好了?当然有,用数据库同义词就可以轻松搞定!

创建同义词步骤如下:

不同服务器跨库访问实现

当数据库在不同服务器上面,用上面的方法就不行了,那如何实现跨服务器访问呢?很简单,看下面↓↓↓

好了,SQL Server跨库跨服务器访问实现就到这了,如果按照步骤一步一步操作的话,相信您也已经实现了,下一篇文章就来谈谈分库分表实现。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对脚本之家的支持。

您可能感兴趣的文章:

  • SQLServer 跨库查询实现方法
  • SQL Server 跨库同步数据
  • MSSQLServer跨服务器连接(远程登录)的示例代码

给现有MVC 项目添加 WebAPI - 小y - 博客园

mikel阅读(896)

来源: 给现有MVC 项目添加 WebAPI – 小y – 博客园

1. 增加一个WebApi Controller, VS 会自动添加相关的引用,主要有System.Web.Http,System.Web.Http.WebHost,System.Net.Http

2. 在App_Start 下创建 WebApiConfig.cs 并注册路由

复制代码
复制代码
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Http;

namespace Libaray.Web.App_Start
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            // Web API 配置和服务

            // Web API 路由
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );
        }
    }
}
复制代码
复制代码

3. 在Global.asax, Application_Start 下添加 WebAPI 配置

复制代码
复制代码
using Libaray.Web.App_Start;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Mvc;
using System.Web.Optimization;
using System.Web.Routing;

namespace Libaray.Web
{
    public class MvcApplication : System.Web.HttpApplication
    {
        protected void Application_Start()
        {
            AreaRegistration.RegisterAllAreas();
            GlobalConfiguration.Configure(WebApiConfig.Register);
            FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
            RouteConfig.RegisterRoutes(RouteTable.Routes);
            BundleConfig.RegisterBundles(BundleTable.Bundles);
        }
    }
}
复制代码
复制代码

4. 在第一步添加的WebApi 中填写相应代码,

复制代码
复制代码
using Libaray.Web.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;

namespace Libaray.Web.Controllers
{
    [RoutePrefix("api/SystemUsers")]
    public class SystemUsersController : ApiController
    {
        [HttpGet, Route("GetUserList")]
        public List<UserModel> GetUserModels()
        {
            UserModelService UserBS = new UserModelService();
            return UserBS.FindList(u => u.isActive == true);
        }

        [HttpGet, Route("GetUser")]
        public UserModel GetUserModel(int id = 0)
        {
            if(id != 0)
            {
                UserModelService UserBS = new UserModelService();
                return UserBS.Find(u => u.Id == id);
            }
            else
            {
                return null;
            }
        }

        [HttpPost, Route("Login")]
        public bool Login(string loginId,string password)
        {
            UserModelService UserBS = new UserModelService();
            return UserBS.ValidateLoginInfo(loginId, password);
        }
    }
}


复制代码
复制代码

5. Run the application and call the API. Example: http://localhost:49919/api/SystemUsers/GetUserList

Web API 强势入门指南 - 微软互联网开发支持 - 博客园

mikel阅读(702)

来源: Web API 强势入门指南 – 微软互联网开发支持 – 博客园

Web API是一个比较宽泛的概念。这里我们提到Web API特指ASP.NET Web API。

这篇文章中我们主要介绍Web API的主要功能以及与其他同类型框架的对比,最后通过一些相对复杂的实例展示如何通过Web API构建http服务,同时也展示了Visual Studio构建.net项目的各种强大。

目录

什么是 Web API

官方定义如下,强调两个关键点,即可以对接各种客户端(浏览器,移动设备),构建http服务的框架。

ASP.NET Web API is a framework that makes it easy to build HTTP services that reach a broad range of clients, including browsers and mobile devices. ASP.NET Web API is an ideal platform for building RESTful applications on the .NET Framework.

Web API在ASP.NET完整框架中地位如下图,与SignalR一起同为构建Service的框架。Web API负责构建http常规服务,而SingalR主要负责的是构建实时服务,例如股票,聊天室,在线游戏等实时性要求比较高的服务。

Picture20

 

为什么要用 Web API

Web API最重要的是可以构建面向各种客户端的服务。另外与WCF REST Service不同在于,Web API利用Http协议的各个方面来表达服务(例如 URI/request response header/caching/versioning/content format),因此就省掉很多配置。

Picture2

 

当你遇到以下这些情况的时候,就可以考虑使用Web API了。

  • 需要Web Service但是不需要SOAP
  • 需要在已有的WCF服务基础上建立non-soap-based http服务
  • 只想发布一些简单的Http服务,不想使用相对复杂的WCF配置
  • 发布的服务可能会被带宽受限的设备访问
  • 希望使用开源框架,关键时候可以自己调试或者自定义一下框架

功能简介

Web API的主要功能

1. 支持基于Http verb (GET, POST, PUT, DELETE)的CRUD (create, retrieve, update, delete)操作

通过不同的http动作表达不同的含义,这样就不需要暴露多个API来支持这些基本操作。

2. 请求的回复通过Http Status Code表达不同含义,并且客户端可以通过Accept header来与服务器协商格式,例如你希望服务器返回JSON格式还是XML格式。

3. 请求的回复格式支持 JSON,XML,并且可以扩展添加其他格式。

4. 原生支持OData

5. 支持Self-host或者IIS host。

6. 支持大多数MVC功能,例如Routing/Controller/Action Result/Filter/Model Builder/IOC Container/Dependency Injection。

Web API vs MVC

你可能会觉得Web API 与MVC很类似,他们有哪些不同之处呢?先上图,这就是他们最大的不同之处。

Picture1

详细点说他们的区别,

  • MVC主要用来构建网站,既关心数据也关心页面展示,而Web API只关注数据
  • Web API支持格式协商,客户端可以通过Accept header通知服务器期望的格式
  • Web API支持Self Host,MVC目前不支持
  • Web API通过不同的http verb表达不同的动作(CRUD),MVC则通过Action名字表达动作
  • Web API内建于ASP.NET System.Web.Http命名空间下,MVC位于System.Web.Mvc命名空间下,因此model binding/filter/routing等功能有所不同
  • 最后,Web API非常适合构建移动客户端服务

Web API vs WCF

发布服务在Web API和WCF之间该如何取舍呢?这里提供些简单地判断规则,

  • 如果服务需要支持One Way Messaging/Message Queue/Duplex Communication,选择WCF
  • 如果服务需要在TCP/Named Pipes/UDP (wcf 4.5),选择WCF
  • 如果服务需要在http协议上,并且希望利用http协议的各种功能,选择Web API
  • 如果服务需要被各种客户端(特别是移动客户端)调用,选择Web API

Web API 实战 (Web API + MongoDB + knockoutjs)

ASP.NET网站上有很多简单的Web API实例,看看贴图和实例代码你就明白怎么用了。这里我们通过一个稍微复杂一点的实例来展示下Web API的功能。

涉及技术

在我们的实例里面用到了:

服务URI Pattern

Action Http verb URI
Get contact list GET /api/contacts
Get filtered contacts GET /api/contacts?$top=2
Get contact by ID GET /api/contacts/id
Create new contact POST /api/contacts
Update a contact PUT /api/contacts/id
Delete a contact DELETE /api/contacts/id

准备工作

1. 下载并安装Mongo DB,步骤看这里

2. Mongo DB C# driver下载可以在nuget搜索mongocsharpdriver。

3. 如果想本地察看数据库中内容,下载MongoVUE

4. Knockoutjs下载可以在nuget搜索knockoutjs。

代码实现

1. 创建项目

创建MVC4 Web Application

1

在Project Template中选择Web API

2

然后项目就创建成了,Controllers里面有一个ValuesController,是自动生成的一个最简单的Web API Controller。

正如我们前面所说,里面引用的是System.Web.Http命名空间。

3

2. 创建model

在model里面添加Contact类

4

代码如下,其中BsonId需要mongocsharpdriver。

1
2
3
4
5
6
7
8
9
public class Contact
    {
        [BsonId]
        public string Id { get; set; }
        public string Name { get; set; }
        public string Phone { get; set; }
        public string Email { get; set; }
        public DateTime LastModified { get; set; }
    }

我们需要添加mongosharpdriver。

7

8

另外我们需要在Model中添加Repository,Controller通过该类来访问Mongo DB。

1
2
3
4
5
6
7
public interface IContactRepository {
        IEnumerable GetAllContacts();
        Contact GetContact(string id);
        Contact AddContact(Contact item);
        bool RemoveContact(string id);
        bool UpdateContact(string id, Contact item);  
    }

ContactRepository的完整实现如下,

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
public class ContactRepository : IContactRepository
    {
        MongoServer _server = null;
        MongoDatabase _database = null;
        MongoCollection _contacts = null;
        public ContactRepository(string connection)
        {
            if (string.IsNullOrWhiteSpace(connection))
            {
                connection = "mongodb://localhost:27017";
            }
            _server = new MongoClient(connection).GetServer();
            _database = _server.GetDatabase("Contacts");
            _contacts = _database.GetCollection("contacts");
            // Reset database and add some default entries
            _contacts.RemoveAll();
            for (int index = 1; index < 5; index++)
            {
                Contact contact1 = new Contact
                {
                    Email = string.Format("test{0}@example.com", index),
                    Name = string.Format("test{0}", index),
                    Phone = string.Format("{0}{0}{0} {0}{0}{0} {0}{0}{0}{0}", index)
                };
                AddContact(contact1);
            }
        }
        public IEnumerable GetAllContacts()
        {
            return _contacts.FindAll();
        }
        public Contact GetContact(string id)
        {
            IMongoQuery query = Query.EQ("_id", id);
            return _contacts.Find(query).FirstOrDefault();
        }
        public Contact AddContact(Contact item)
        {
            item.Id = ObjectId.GenerateNewId().ToString();
            item.LastModified = DateTime.UtcNow;
            _contacts.Insert(item);
            return item;
        }
        public bool RemoveContact(string id)
        {
            IMongoQuery query = Query.EQ("_id", id);
            WriteConcernResult result = _contacts.Remove(query);
            return result.DocumentsAffected == 1;
        }
        public bool UpdateContact(string id, Contact item)
        {
            IMongoQuery query = Query.EQ("_id", id);
            item.LastModified = DateTime.UtcNow;
            IMongoUpdate update = Update
                .Set("Email", item.Email)
                .Set("LastModified", DateTime.UtcNow)
                .Set("Name", item.Name)
                .Set("Phone", item.Phone);
            WriteConcernResult result = _contacts.Update(query, update);
            return result.UpdatedExisting;
        }
    }

3. 添加Controller

右键Controllers目录选择添加Controller

5

选择Empty API controller,将Controller命名为ContactsController

6

添加如下代码,可以看到Controller中的API方法名就是以http verb命名的。

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
public class ContactsController : ApiController
    {
        private static readonly IContactRepository _contacts = new ContactRepository(string.Empty);
        public IQueryable Get()
        {
            return _contacts.GetAllContacts().AsQueryable();
        }
        public Contact Get(string id)
        {
            Contact contact = _contacts.GetContact(id);
            if (contact == null)
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
            return contact;
        }
        public Contact Post(Contact value)
        {
            Contact contact = _contacts.AddContact(value);
            return contact;
        }
        public void Put(string id, Contact value)
        {
            if (!_contacts.UpdateContact(id, value))
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
        }
        public void Delete(string id)
        {
            if (!_contacts.RemoveContact(id))
            {
                throw new HttpResponseException(HttpStatusCode.NotFound);
            }
        }
    }

4. 添加View

首先添加Knockoutjs库,

9

Knockoutjs通过MVVM模式来实现动态html绑定数据,如下图,其中View-Model是客户端的JavaScript object保存的model数据。

webapi_ef16

先打开HomeController,里面添加一个新的Action代码如下,因为我们要在MVC中对于ContactsController添加对应的View。

1
2
3
4
5
6
7
public ActionResult Admin()
        {
            string apiUri = Url.HttpRouteUrl("DefaultApi", new { controller = "contacts", });
            ViewBag.ApiUrl = new Uri(Request.Url, apiUri).AbsoluteUri.ToString();
            return View();
        }

然后右键Admin方法,选择添加View

10

选择Create strongly-typed view,在model class中选择Contact类。

11

添加View的完整代码,注意view中我们通过js去访问WebAPI,以及通过动态绑定将数据呈现在网页上。

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
@model WebAPIDemo.Models.Contact
@{
    ViewBag.Title = "Admin";
}
@section Scripts {
  @Scripts.Render("~/bundles/JQueryval")
  <script type="text/JavaScript" src="@Url.Content("~/Scripts/knockout-2.3.0.js")"></script>
  <script type="text/javascript">
      function ProductsViewModel() {
          var self = this;
          self.products = ko.observableArray();
          var baseUri = '@ViewBag.ApiUrl';
          self.create = function (formElement) {
              // If valid, post the serialized form data to the web api
              $(formElement).validate();
              if ($(formElement).valid()) {
                  $.post(baseUri, $(formElement).serialize(), null, "json")
                      .done(function (o) { self.products.push(o); });
              }
          }
          self.update = function (product) {
              $.ajax({ type: "PUT", url: baseUri + '/' + product.Id, data: product });
          }
          self.remove = function (product) {
              // First remove from the server, then from the UI
              $.ajax({ type: "DELETE", url: baseUri + '/' + product.Id })
                  .done(function () { self.products.remove(product); });
          }
          $.getJSON(baseUri, self.products);
      }
      $(document).ready(function () {
          ko.applyBindings(new ProductsViewModel());
      })
  </script>
}
<h2>Admin</h2>
<div class="content">
    <div class="float-left">
    <ul id="update-products" data-bind="foreach: products">
        <li>
            <div>
                <div class="item">ID</div> <span data-bind="text: $data.Id"></span>
            </div>
            <div>
                <div class="item">Name</div>
                <input type="text" data-bind="value: $data.Name"/>
            </div>
            <div>
                <div class="item">Phone</div>
                <input type="text" data-bind="value: $data.Phone"/>
            </div>
            <div>
                <div class="item">Email</div>
                <input type="text" data-bind="value: $data.Email"/>
            </div>
            <div>
                <div class="item">Last Modified</div> <span data-bind="text: $data.LastModified"></span>
            </div>
            <div>
                <input type="button" value="Update" data-bind="click: $root.update"/>
                <input type="button" value="Delete Item" data-bind="click: $root.remove"/>
            </div>
        </li>
    </ul>
    </div>
    <div class="float-right">
    <h2>Add New Product</h2>
    <form id="addProduct" data-bind="submit: create">
        @Html.ValidationSummary(true)
        <fieldset>
            <legend>Contact</legend>
            @Html.EditorForModel()
            <p>
                <input type="submit" value="Save" />
            </p>
        </fieldset>
    </form>
    </div>
</div>

接下来在_layout.cshtml中添加一个admin页面的链接如下

1
2
3
4
5
6
<ul id="menu">
    <li>@Html.ActionLink("Home", "Index", "Home", new { area = "" }, null)</li>
    <li>@Html.ActionLink("API", "Index", "Help", new { area = "" }, null)</li>
    <li>@Html.ActionLink("Admin", "Admin", "Home")</li>
</ul>

5. 测试与调试

大功告成,直接运行下我们的作品,我们的admin链接也显示在右上角,

12

Admin页面的样子,Contact list是动态加载进来的,可以通过这个页面做添加,修改,删除的操作。

13

通过IE network capture来查看请求内容,

重新加载页面,可以看到回复的格式为JSON,

14

JSON内容就是我们mock的一些数据。

image

接下来我们修改,删除,又添加了一条记录,可以看到使用了不同的http method。

image

通过前面安装的mongovue来查看下DB种的数据,先添加的user也在其中,令我感到欣慰。。。

image

其实还有两个有趣的实例,不过文章一写就长了,不好意思耽误大家时间,只好先放放,以后再写。

一步一步使用ABP框架搭建正式项目系列教程 - tkbSimplest - 博客园

mikel阅读(735)

来源: 一步一步使用ABP框架搭建正式项目系列教程 – tkbSimplest – 博客园

研究ABP框架好多天了,第一次看到这个框架的名称到现在已经很久了,但由于当时内功有限,看不太懂,所以就只是大概记住了ABP这个名字。最近几天,看到了园友@阳光铭睿的系列ABP教程,又点燃了我内心要研究ABP框架的熊熊烈火。@阳光铭睿的系列ABP教程我已经看完了,并且ABP官网(http://www.aspnetboilerplate.com/)的文档也看得差不多了。同时也发现了园友@阳光铭睿的系列ABP教程很多都是从官网翻译过来的,而且翻译水平有限,很难不让对英语稍微感兴趣的我挑刺儿(可能@阳光铭睿兄太忙吧),嘿嘿,还望@阳光铭睿兄见谅啊!同时,也看见了很多园友都希望用有园友能使用这个框架做一个项目Demo(其实官网也有Demo,感觉稍微简单点儿)。而我现在要着手一个新的项目,也打算用这个框架,所以想要将我一步一步用这个框架的做项目的步骤和想法跟大家分享出来。但提前声明,我不会将源码分享出来,一是因为这是个正式项目,涉及公司隐私等保密协议;二是因为衷心希望提高.Net开发人员的动手能力,不培养“伸手党”,提高.Net生态圈“保(程)卫(序)者(员)”的素质。但是,有任何问题,我会更大家一起在评论区互动。在此,先谢谢大家的支持!

帮助你学习的广告:关于ABP框架的中文“理论”教程你可以查看ABP理论典藏版,如果您英文好一些的话,那么我建议您去官网阅读英文文档【建议程序员们一定好好学英语,可以获取很多高级技术的一手资料,否则您对技术的掌握和理解就只能取决于翻译者的水平了】

系列教程会按以下顺序展开,很可能会有所变动,以下只是类似一个大纲(也是使用ABP框架创建应用的建议步骤),具体请以具体文章为主!

1.使用boilerplate模版创建解决方案

2.创建实体类

3.创建DbContext

4.创建数据库迁移

5.定义仓储

6.实现仓储

7.构建应用层服务

8.数据校验

9.构建Web API服务

10.开发UI层

11.本土化

12.单元测试

13.总结

ABP理论基础学习,请移步至ABP框架理论研究总结(典藏版)

要学习Module-Zero,请移步至《Module-Zero 学习目录》

织梦DEDECMS转WordPress的教程方法汇总 - 奶爸建站笔记

mikel阅读(866)

来源: 织梦DEDECMS转WordPress的教程方法汇总 – 奶爸建站笔记

织梦DEDECMS转WordPress的教程方法汇总

织梦dedecms是很多站长使用的国产CMS系统之一,近日织梦官方发表通知称商业用户需要购买授权费才能使用,费用为5800元/年,所以商用的站长最好更换掉其他免费开源的建站系统,例如WordPress,或者购买商业授权,毕竟国内的版权已经越来越严格了。

这里奶爸给大家总结几个可以方便把DedeCMS转移到WordPress系统的方法

DedeCMS转WordPress方法一:通过RSS导出然后导入

这个方法的原理很简单,首先,在DedeCMS系统里面把全站的内容都导出为RSS格式,然后利用WordPress的导入功能,导入rss文件,就可以自动把之前的文章复制过来。

具体的大家可以参考下面这个步骤:

https://github.com/zyfcjtc/dedecms_to_wordpress

  1. 在dedecms的dede文件夹(后台文件夹)下找到makehtml_rss_action.php文件,对其进行编辑,找到代码:
    echo "完成所有文件更新!";

    在其下面添加代码:

    echo "<a href='/rss.xml' target='_blank'>浏览…</a>";

    作用是在生成rss.xml文件后方便点击查看生成结果。

  2. 在dedecms的include文件夹下找到arc.rssview.class.php文件,并对其进行编辑,找到代码:
    $murl = $GLOBALS['cfg_cmspath']."/data/rss/".$this->TypeID.".xml";

    修改为:

    $murl = $GLOBALS['cfg_cmspath']."/rss.xml";

    作用是修改生成的rss.xml文件路径,让该文件保存在网站根目录

    再向下找到以下代码并将其删除:

    $orwhere .= "And (arc.typeid in (".GetSonIds($this->TypeID,$this->TypeFields['channeltype']).") )";

进入后台——生成——更新RSS文件,把“单个类目最大记录数”改为全站的文章数量,然后点击“开始更新”,更新完成后,网站根目录下会生成一个全站rss.xml文件,可通过 http://网站域名/rss.xml 查看。 附:如果生成的rss.xml文件有错误,在根目录下的templets文件夹中的plus文件夹里找到rss.htm文件,对其进行编辑,找到代码:

<description><![CDATA[[field:description function='html2text(@me)'/]]]></description>

修改为:

<description><![CDATA[[field:description/]]]></description>

注:wordpress的编码是utf8,如果dedecms使用的不是utf8的,导入前请先转换编码为utf8。 具体如何查看: 找到common.inc.php文件,里面的

$cfg_version = 'V57_UTF8_SP2';
$cfg_soft_lang = 'utf-8';
$cfg_soft_public = 'base';

即可知道是什么编码

导入织梦RSS步骤

  1. 织梦DedeCMS文章标题等基本数据导入wordpress站点 进入wordpress后台admin => 工具 => 导入 => RSS, 上传导入生成的织梦全站RSS文件。 导入过程中有可能会超时,重新上传RSS即可,不会重复导入。 注:我通过修改rss-import.php里的get_posts function,实现了同时导入织梦文章id到wordpress. 具体请见这里: https://github.com/zyfcjtc/dedecms_to_wordpress/blob/main/get_posts.php
  2. 导入织梦CMS文章全文到wordpress站点 织梦文章的数据存储在dede_addonarticle数据库的body字段中,现在需要把body字段的内容转到wordpress数据库的wp_posts数据库的 post_content字段里。这个转换需要使用一个桥梁——那就是dede_archives数据表,即dede_addonarticle上body的内容先转到dede_archives上,再从 dede_archives转到wp_posts的post_content里。这两次转换的匹配点,分别是织梦里的文章id,以及Wordpress里已经导入了的文章标题 (这与织梦里的文章标题是一样的)。具体步骤如下: 进入phpmyadmin,选择dedecms网站使用的数据库,在SQL输入框中执行以下SQL语句,在织梦数据库的dede_archives表上,添加字段body
    ALTER TABLE dede_archives ADD body longtext NOT NULL

    然后再执行以下SQL语句把dede_addonarticle数据表中的body字段内容导入到dede_archives的body字段,语句以dede_addonarticle的aid 和dede_archives的id为匹配点:

    UPDATE dede_archives,dede_addonarticle
    SET dede_archives.body = dede_addonarticle.body  
    WHERE dede_archives.id = dede_addonarticle.aid

    接着通过phpmyadmin导出功能把dede_archives数据表导出,然后再通过导入功能把该数据表导入wordpress网站使用的数据库中,使其与 wp_posts数据库处在同一个数据库里。

    下面再次使用SQL语句把dede_archives的body导入到wp_posts上的post_content上,以文章标题为匹配点(前提是文章标题都是唯一的) 以ID为匹配点:

    UPDATE wp_posts,dede_archives  
    SET wp_posts.post_content = dede_archives.body  
    WHERE wp_posts.id = dede_archives.id

    至此文章内容部分转换完成!

DedeCMS转WordPress方法二:数据库转换

大概思路就是把dedecms的数据库内容导入到wordpress,并且通过一些数据库命令操作,让本来dedecms的数据库转变为wordpress的,这个方法网上的教程都比较老,不推荐尝试。

DedeCMS转WordPress方法三:复制或者采集

这个方法相对来说是奶爸推荐大家操作的,因为最保险,只不过原网站数据量太大的话,操作起来非常费时间。

具体步骤:

  1. 先搭建一个临时的WordPress网站,教程参考:安装WordPress教程
  2. 在WordPress后台,文章,文章分类里面设置好原网站(DEDECMS)的分类。
  3. 手动一篇文章一篇文章的复制或者使用采集软件(淘宝有帮忙写采集规则的店铺),把文章全部采集过来。
  4. 利用elementor这类型的构建器设计网站首页的效果。
  5. 域名切换为原dedecms网站的域名。
  6. 设置伪静态和301跳转规则,实现可以通过老网站的url跳转到新的url,减少SEO影响。

织梦转为WordPress后的注意事项

一、注意做伪静态和301跳转

虽然织梦和WordPress都是php的程序,但是他们的伪静态规则是不一样的,所以从dedecms转到WordPress之后,之前dedecms的文章页链接打开会报错,这就需要自己做301跳转规则来实现老dedecms网站的文章链接跳转到新WordPress网站的链接。

WordPress做301跳转的话也可以使用插件帮忙,例如Redirection

二、网站页面需要重新设计

不管用哪种方法把DEDECMS转换到WordPress,网站的页面设计都会发生变化的,所以网站文章全部复制过来之后,也需要自己从零开始设计网站的页面。新手的话推荐使用elementor构建器操作,或者直接花钱找人帮你设计,这是比较靠谱的方法。

12306抢票算法居然被曝光了!!!居然是redis实现的 - 程序员小饭 - 博客园

mikel阅读(682)

来源: 12306抢票算法居然被曝光了!!!居然是redis实现的 – 程序员小饭 – 博客园

导读

相信大家应该都有抢火车票的经验,每年年底,这都是一场盛宴。然而你有没有想过抢火车票这个算法是怎么实现的呢? 应该没有吧,咱们今天就来一一探讨。其实并没有你想的那么难

bitmap与位运算

redis的bitmap基本使用咱们之前已经介绍过了,如果不是很熟悉的朋友可以看看这里 redis bitmap的基本操作和应用

今天在这里咱们主要是先回顾一下位运算

12306抢票算法详解

我们以北京到西安这趟高铁为例,比如我的路线就是从北京到西安,车上如果只剩最后一张票了,那么如果有其他人,在北京到西安这条路线之间买任何一站,那么我都是买不了票的,换句话说,对于单个座位来说,必须是起点到目的地之间的所有站,都没有人买的话,那么才能被算是有票状态。

所以我们可以尝试用bitmap结合上位操作来实现这种场景,以上述北京到西安为例,我们把问题简化

  • 比如一个火车上只有4个座位
  • 北京到西安,一共是4站,其实是三个区间的,分别为北京->石家庄,石家庄->郑州,郑州->西安

首先我们给每个区间构建一个空位图(0为有票,1为无票)

接下来,比如有人买了一张从北京到西安的票

买票这个动作,比如被分配到的座位是编号为1的座位,那么我们直接把北京到西安的所有站,1号座位全部设置为1,如下图

接下来又有人买了一张从石家庄到西安的票

比如这次分配的是座位2,那么我们把石家庄到西安的所有票全部设置为1就行了,如下图

如何知道还剩几张票?

其实解决这个问题很简单,我们直接把上述位图做一个或操作就可以了

或操作结果有几个0,则说明还剩几张票。

总结

其实解决这个问题主要在于位图的构建,因为火车票对于某一个座位来说,只要起点到终点中间某一个区间被占用了(置为1),那么整个座位都是无效的这个特点,很容易想到用或操作的结果来判断买票结果,我们这里只用了4位是为了方便说明问题,实际中应该是火车上有多少座位,位图的长度就应该是多少。 好了,关于抢票算法我们就介绍到这里,你有没有Get到呢?或者你有没有更好的实现方法呢?

redis中setbit(位操作)的实际应用

mikel阅读(852)

来源: redis中setbit(位操作)的实际应用

edis的五种数据类型相信大家都非常清楚了,任何人问你基本都能脱口而出(string,hash,list,set,zset)。如果还不清楚,建议先看看redis基础,但是还有这几种类型很多人会忽略到。比如 Setbit(位操作),GEO(地理位置信息)等等。

今天我们就来淦一淦setbit,看一看实际场景中到底是怎么用的,以及有哪些优势?

我们先来回顾一下setbit吧,大家知道位操作,只有两个值,0和1,8个位正好是1b,所以位操作是非常节省空间的一种操作。

1 Byte(B) = 8 bit

1 Kilo Byte(KB) = 1024B

1 Mega Byte(MB) = 1024 KB

1 Giga Byte (GB)= 1024 MB

在redis中他的用法也非常简单, 基本语法如下:

redis 127.0.0.1:6379> Setbit KEY_NAME OFFSET

例子:

redis> SETBIT bit 10086 1  #把第10086个位置设置为1
(integer) 0

redis> GETBIT bit 10086    #获取第10086个位置的值  看是0还是1
(integer) 1

redis> GETBIT bit 100   # bit 默认被初始化为 0
(integer) 0

其实就是把某个位标记为1或者0而已,但是它的好处在于非常节省空间。另外既然是位,就会涉及到或运算或者与运算(后面会有实例)。

我们来看一个实例吧

场景: 1亿个用户,每个用户登陆/做任意操作,记为 今天活跃,否则记为不活跃。

每周评出: 有奖活跃用户: 连续7天活动

每月评,等等…

其实简单说就是统计一下连续7天(或者连续30天)有多少人连续登陆过

咱们先来想一想传统的方案

很容易就会想到只要用户登陆了,我在表中插入一条数据,并且记录上对应的日期,然后用mySQL里面的记录来逐个判断,类似于这样:

Userid   date        active

1       2020-07-27  1
1       2020-07-26   1
2       2020-07-27  1
...

但这样是存在一些问题的,主要的问题在于用户量高达1亿,每个用户登陆一次就远远的超过mySQL的极限了,更不要说统计一星期了,而且用上group ,sum运算,计算也是非常慢的。所以在这种用户量大,而且统计比较简单的问题上,咱们可以运用位(setbit)操作来解决问题。

先分析一下思路,对于某一天来说,我们可以把这一天想像成一根小木棍,分成了不同的段落,每个段落对应的就是用户的位(因为有user_id),默认值都是0,只要有人登陆了,就把对应的用户的位置标为1即可。

图片

如上图所示,这个就是一天的登陆情况,user_id为6和user_id为8的用户登陆过。其余的都为没有登陆过。因为这个是位操作,所以占的空间很小,1亿的用户,所占的空间也就不到12M。

一天的问题咱们解决了,如何解决他们是否连续登陆过呢?

我们可以用上多个”木棍”

图片

我们可以把每一天作为一个键,然后每天对用户登陆状态进行标记,在最后用每天做一个”与运算”就可以准确的知道哪些用户连续登陆了。

其实总结一下过程如下:

1、记录用户登陆:

每天按日期生成一个位图, 用户登陆后,把user_id位上的bit值置为1

2:、把1周的位图  and 计算,

位上为1的,即是连续登陆的用户

代码实现如下:

redis 127.0.0.1:6379> setbit mon 100000000 0

(integer) 0

redis 127.0.0.1:6379> setbit mon 3 1

(integer) 0

redis 127.0.0.1:6379> setbit mon 5 1

(integer) 0

redis 127.0.0.1:6379> setbit mon 7 1

(integer) 0

redis 127.0.0.1:6379> setbit thur 100000000 0

(integer) 0

redis 127.0.0.1:6379> setbit thur 3 1

(integer) 0

redis 127.0.0.1:6379> setbit thur 5 1

(integer) 0

redis 127.0.0.1:6379> setbit thur 8 1

(integer) 0

redis 127.0.0.1:6379> setbit wen 100000000 0

(integer) 0

redis 127.0.0.1:6379> setbit wen 3 1

(integer) 0

redis 127.0.0.1:6379> setbit wen 4 1

(integer) 0

redis 127.0.0.1:6379> setbit wen 6 1

(integer) 0

redis 127.0.0.1:6379> bitop and  res mon feb wen

(integer) 12500001

 

如上例,优点为:

1、节约空间, 1亿人每天的登陆情况,用1亿bit,约1200WByte,约10M 的字符就能表示;

2、计算方便。

图片

尝试打开或创建物理文件时,CREATE FILE遇到操作系统错误5 (拒绝访问_麻木博客-CSDN博客

mikel阅读(1831)

来源: 尝试打开或创建物理文件时,CREATE FILE遇到操作系统错误5 (拒绝访问_麻木博客-CSDN博客

错误图
使用SQL Server Management Studio 数据库查询语句创建数据出现如下错误,语法没错,但是就是创建不了

 

尝试打开或创建物理文件 REATE FILE 遇到操作系统错误 5(拒绝访问)

 

原因解析
是因为这个盘符少了users这个用户组,所以你现在的用户没有权限

 

怎么做?
右击该盘,选择属性

 

选择安全-编辑

 

选择添加,在输入对象名称下方输入 users,点击确定

 

 

 

等待配置完成,此账户则拥有权限

 

权限有了就可以继续去执行查看是否可以

 

再次执行语句即可发现命令执行成功没报错了

————————————————
版权声明:本文为CSDN博主「麻木博客」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_44708572/article/details/106832675

c#结束本次循环,结束循环return、break、continue的用法_橙cplvfx-技术踩坑记-CSDN博客

mikel阅读(1336)

来源: c#结束本次循环,结束循环return、break、continue的用法_橙cplvfx-技术踩坑记-CSDN博客

break //是跳出当前循环,
continue //跳过本次循环,执行下一次循环
return //是终止执行当前“函数”或“方法”,并可返回一个值
break语句:
break语句会使运行的程序立刻退出包含在最内层的循环或者退出一个switch语句。由于它是用来退出循环或者switch语句,所以只有当它出现在这些语句时,这种形式的break语句才是合法的。

程序代码

continue语句:

continue语句和break语句相似。所不同的是,它不是退出一个循环,而是开始循环的一次新迭代。
continue语句只能用在while语句、do/while语句、for语句、或者for/in语句的循环体内,在其它地方使用都会引起错误!

程序代码

 

return语句:

return语句就是用于指定函数返回的值。return语句只能出现在函数体内,出现在代码中的其他任何地方都会造成语法错误!
当执行return语句时,即使函数主体中还有其他语句,函数执行也会停止! return;不返回什么,直接跳出正在执行的函数.不执行return后面的代码
————————————————
版权声明:本文为CSDN博主「cplvfx」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/cplvfx/article/details/120151537

thinkphp3.2+workerman(GatewayWorker)+ Layim做即时通讯_菜鸟的博客-CSDN博客

mikel阅读(826)

来源: thinkphp3.2+workerman(GatewayWorker)+ Layim做即时通讯_菜鸟的博客-CSDN博客

此博客参考https://fly.layui.com/jie/12624/

一,程序实现的逻辑:

后端还是和以前一样,传值给前端。
前端与GatewayWorker建立连接获取特定的连接id
(这样前端就有用户id和连接id)

3.前端通过ajax方法发送用户id和链接id给后端,后端进行绑定

4.监控前端发送消息,发送人id,当触发该事件,传到后端,后端根据用户id值,发送给特定的人

5.监控前端接收消息,当有新消息过来就在页面执行相应的代码

 

 

二,环境的搭建

1,下载GatewayWorker

(地址:http://www.workerman.net/download/GatewayWorker-for-win.zip)

2,修改Applications/YourApp/目录下的start_geteway.php 24行左右 修改为$gateway = new Gateway(“websocket://0.0.0.0:8282″);(端口号和前端保持一致)

3,下载workerman的GatewayClient(https://github.com/walkor/GatewayClient);

4,将下载好的Gateway重命名为Gateway.class.php并修改命名空间为namespace Org\Util;

 

将前面重命名Gateway.class.php复制到thinkphp/library/org/util/目录下
双击启动GatewayWorker根目录下的start_for_win.bat()
如果出现下面:

 

说明启动成功

如果没有启动成功将有以下解决方法

将php设置为全局环境变量(网上有很多方法)测试是否成功在命令行执行php –v,如果提示“php不是内部或外部命令“之类的就说明没有设置好
如果有下图之类的提示

上图那样有三个问题

php_mcrypt.dll – 找不到指定的模块。
php_pdo.dll – 找不到指定的模块。
proc_open() has been disabled for security reasons.
找不到指定模块:解决方法打开php.ini文件,查找模块名(比如上图php_pdo.dll);找到在这句前面加上“;”。注释掉即可

has been disabled for security reasons.解决方法:就是把这个方法的禁止打开,也是在php.ini文件修改;找到这个方法,直接删掉即可

 

三,代码部分

后端部分
因为该篇主要讲即时通讯的实现,所以用户信息获取那部逻辑就不做展示。
后端绑定和发送信息

后端接收前端数据的时候,还做了把数据存入数据库的处理,这样就可以实现数据的持久化

 

前端部分

引入文件
<script src=”__PUBLIC__/JQuery/2.0.0/JQuery.js”></script>

<link rel=”stylesheet” href=”__PUBLIC__/layim/css/layui.mobile.css”>

<script src=”__PUBLIC__/layim/layui.js”></script>
自己信息的初始化
layui.use(‘mobile’, function(layim){

var mobile = layui.mobile

,layim = mobile.layim;

layim.config({

init: {

mine: {

“username”: “{$user.name}” //我的昵称

,”id”: “user{$user.id}” //我的ID

,type: ‘kh’//类型

,”avatar”: “{$user.img}” //我的头像

}

} , chatTitleColor:”#fff”,

chatLog: ‘/chat/log/’

});

(3)创建客服信息

layim.chat({

id: “kf{$kf.id}”

,name: ‘客服{$kf.id}’

,type: ‘kefu’ //friend、group等字符,如果是group,则创建的是群聊

,avatar: ‘/luntan/{$kf.img}’

});

(4)建立连接

var socket = new WebSocket(‘ws://localhost:8282’);//服务器改成ip地址加端口号

连接方法

第一次连接,会收到两次信息,接收后端传来数据也是在这里处理

 

我们可以在onmessage方法里面进行处理,取出连接值,再传给后端,这样就绑定好了

除了是登录事件,登出事件,就是后端正常我们自己发送的数据了

我们把接收到的值,再进行展示,这样就可以实现我们需要的效果了。

//连接成功时触发
socket.onopen = function(e){

//这里可以写一些提示语

}

//监听收到的消息
socket.onmessage = function(e){
var data = e.data

// console.log(e);

data=data.replace(‘\r\n’,”);//删除换行符

var arr=data.split(” “);

if(arr[0]==”Hello”){

var client_id=arr[1].trim();

//绑定client_id

$.ajax({type: “POST”, url: “{:U(‘Workerman/bind’)}”, data: {

‘uid’:”user{$user[‘id’]}”,

‘client_id’: client_id}, dataType: ‘json’, success: function (res) {

console.log(res);

}});

}else if(arr[1]!=”login”&&arr[1]!=”logout”){

//接收服务器信息

data=JSON.parse(data);

if(data[‘sendid’]==”kf{$kf.id}”){

$(“#link”).html(“可点击进行评价”);

//console.log(data[‘sendname’]);

var obj = {};

obj = {

username:data[‘sendname’]

,avatar: data[‘img’]

,id: ‘kf1’

,type:”kefu”

,content: data[‘content’]

}

layim.getMessage(obj);

}

}

(5)监听用户发送信息

layim.on(‘sendMessage’, function(data){

console.log(data);

var sendid=data.mine.id;

var sendtype=”kh”;

var sendname=data.mine.username;

var sendimg=data.mine.avatar;

var contents=data.mine.content;

var toid=data.to.id;

var toname=data.to.name;

var totype=data.to.type;

var toimg=data.to.avatar;

var talkid=data.mine.id+”|”+data.to.id;

var talktip=data.mine.username+”|”+data.to.name;

$.ajax({

type: “POST”,

url: “{:U(‘User/getcontents’)}”,

data: {

‘sendid’:sendid,

‘sendname’: sendname,

‘sendtype’:sendtype,

‘sendimg’:sendimg,

‘contents’:contents,

‘toid’:toid,

‘toname’:toname,

‘totype’:totype,

‘toimg’:toimg,

‘talkid’:talkid,

‘talktip’:talktip,

},

dataType: ‘json’,

success: function (res) {

console.log(res);

}

})
至此即时通讯的修改就出来

 

 

四,数据的持久化,跨设备储存

把聊天记录根据自己的需求存入数据库

单用户进入聊天界面时,根据是谁与谁的聊天,读取相应数据,然后前端得到数据再进行展示。

注意layim聊天记录的要求时间戳是精确到毫秒的

(1),php获取当前毫秒的方法

public function getMillisecond() {

$time = explode (” “, microtime () );

$time = $time [1] . ($time [0] * 1000);

$time2 = explode ( “.”, $time );

$time = $time2 [0];

return $time;

}
(2),前端初始化数据

localStorage.clear();
(3),前端获取数据,并展示

$.ajax({type: “POST”, url: “{:U(‘User/getmsg’)}”, data: {

‘uid’:”user{$user[‘id’]}”,’kid’:”kf{$kf.id}”},

dataType: ‘json’, success: function (res) {

if(res.stu==1){

$(“#link”).html(“获取历史记录成功”);

var msg=res.msg;

// console.log(msg)

for ($i=0;$i<msg.length;$i++){

var isme=false;

if(“user{$user[‘id’]}”==msg[$i].sendid){

isme=true;

}

var obj = {};

obj = {

username:msg[$i].sendname//消息来源用户名

,avatar: msg[$i].sendimg //消息来源用户头像

,id: msg[$i].sendid//消息的来源ID(如果是私聊,则是用户id,如果是群聊,则是群组id)

,type:msg[$i].totype //聊天窗口来源类型,从发送消息传递的to里面获取

,content: msg[$i].contents //消息内容

,mine: isme

,timestamp: parseInt(msg[$i].time) //服务端时间戳毫秒数。注意:如果你返回的是标准的 unix 时间戳,记得要 *1000

}

layim.getMessage(obj);

}

}

}})
这样数据的持久化算是实现了

不过这里还有一个小bug,当执行layim.getMessage()如果是对方数据的时候就有声音提示(也就是上面isme变量为false的时候),所以一打开这个页面就有声音提示,这个是默认打开的,因此我们要把他关闭了

在layim.config里面最后加上“,voice: false“

然后我们自己写收到实时信息的时候发送提示音

Js发送语音提示方法

function playSound() {

var borswer = window.navigator.userAgent.toLowerCase();

if ( borswer.indexOf( “ie” ) >= 0 )

{

//IE内核浏览器

var strEmbed = ‘<embed name=”embedPlay” src=”/Public/layim/css/modules/layim/voice/default.wav” autostart=”true” hidden=”true” loop=”false”></embed>’;

if ( $( “body” ).find( “embed” ).length <= 0 )

$( “body” ).append( strEmbed );

var embed = document.embedPlay;

embed.volume = 100;

} else

{

//非IE内核浏览器

var strAudio = “<audio id=’audioPlay’ src=’/Public/layim/css/modules/layim/voice/default.wav’ hidden=’true’>”;

if ( $( “body” ).find( “audio” ).length <= 0 )

$( “body” ).append( strAudio );

var audio = document.getElementById( “audioPlay” );

//浏览器支持 audion

audio.play();

}
然后再自己需要提示音的时候调用这个方法即可

 

效果图

 

 

 

 

 

最后提示:

如果在服务器搭建的时候记得打开你程序设定的端口的防火墙
命令行窗口不能关闭
以上都是在windows系统搭建的 ,linux搭建这种websocket环境,网上也有很方法
Layim如何修改默认展示的聊天记录的条数
因为layim默认展示20条聊天记录,多的话,以前的记录会被折叠

可以修改lay\modules\mobile.js文件 查找“c=20”后面的条数修改为你需要展示的条数,然后清除浏览器缓存即可(此方法适用于引入moblie的手机版js,pc端可去试试其他文件)

 

 

如若有误或者有其他问题请与我交流:2359582968(微信qq同号)
————————————————
版权声明:本文为CSDN博主「一个菜鸡路上不肯回头的人」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_38211838/article/details/82020708