Home  >  Article  >  WeChat Applet  >  Summary of five pitfalls encountered in PHP WeChat public account development

Summary of five pitfalls encountered in PHP WeChat public account development

高洛峰
高洛峰Original
2017-03-21 15:54:352945browse

This article mainly introduces the five pitfalls of PHP WeChat public account development in detail. It has certain reference value. Interested friends can refer to the

menu. The reply is that the XML file needs to be processed. Based on the XML file returned by WeChat, we can get the unique identifier of each WeChat user relative to the WeChat official account. The mechanism of the WeChat public platform is simply that we output a fixed-format XML file ourselves, and then the WeChat APP is responsible for parsing it, getting the information we want, and then processing the information in a unified manner.

The sixth pit, If you look at the WeChat document, then it will definitely kill you, as shown above. The ToUserName and FromUserName here must be clearly distinguished. Remember, never write them backwards. The user is A→B for WeChat, so WeChat is the other way around for the user. It seems that it should be made clear now. .

Summary of five pitfalls encountered in PHP WeChat public account development


/// 
 /// 接收微信发送的XML消息并且解析
 /// 
 private void ReceiveXml()
 {
 try
 {
  Stream requestStream = System.Web.HttpContext.Current.Request.InputStream;
  byte[] requestByte = new byte[requestStream.Length];
  requestStream.Read(requestByte, 0, (int)requestStream.Length);
  string requestStr = Encoding.UTF8.GetString(requestByte);

  if (!string.IsNullOrEmpty(requestStr))
  { 

  //封装请求类
  XmlDocument requestDocXml = new XmlDocument();
  requestDocXml.LoadXml(requestStr);
  XmlElement rootElement = requestDocXml.DocumentElement;
  WxXmlModel WxXmlModel = new WxXmlModel();
  if (rootElement != null)
  {
   WxXmlModel.ToUserName = rootElement.SelectSingleNode("ToUserName") == null ? "" : rootElement.SelectSingleNode("ToUserName").InnerText;
   WxXmlModel.FromUserName = rootElement.SelectSingleNode("FromUserName") == null ? "" : rootElement.SelectSingleNode("FromUserName").InnerText;
   WxXmlModel.CreateTime = rootElement.SelectSingleNode("CreateTime") == null ? "" : rootElement.SelectSingleNode("CreateTime").InnerText;
   WxXmlModel.MsgType = rootElement.SelectSingleNode("MsgType") == null ? "" : rootElement.SelectSingleNode("MsgType").InnerText;
   switch (WxXmlModel.MsgType)
   {

   case "text"://文本
    WxXmlModel.Content = rootElement.SelectSingleNode("Content") == null ? "" : rootElement.SelectSingleNode("Content").InnerText;
    break;
   case "image"://图片
    WxXmlModel.PicUrl = rootElement.SelectSingleNode("PicUrl") == null ? "" : rootElement.SelectSingleNode("PicUrl").InnerText;
    break;
   case "event"://事件
    WxXmlModel.Event = rootElement.SelectSingleNode("Event") == null ? "" : rootElement.SelectSingleNode("Event").InnerText;
    if (WxXmlModel.Event != "TEMPLATESENDJOBFINISH")//关注类型
    {
    WxXmlModel.EventKey = rootElement.SelectSingleNode("EventKey") == null ? "" : rootElement.SelectSingleNode("EventKey").InnerText;
    }
    break;
   default:
    break;
   }
  }
  ResponseXML(WxXmlModel);//回复消息
  }

 

 }
 catch (Exception ee)
 {
  //记录错误日志
 }
 }

 /// 
 /// 回复消息
 /// 
 /// 
 private void ResponseXML(WxXmlModel WxXmlModel)
 {
 string XML = "";
 switch (WxXmlModel.MsgType)
 {
  case "text"://文本回复
  var info = oauth.GetUserInfo(Tools.WA_GetAccess_Token.IsExistAccess_Token(), WxXmlModel.FromUserName);
  Tools.WAEntity.OAuthUser user = Tools.JsonHelper.ParseFromJson(info);
  var content = WxXmlModel.Content.ToUpper();
  string NcbActUrl = ConfigurationManager.AppSettings["NcbActUrl"];
  string appid = ConfigurationManager.AppSettings["AppID"];
  if (content.Contains("T"))//接受的文字如果包含T
  {
   //业务处理
  }
  else
  {
   XML = ResponseMessage.ReText(WxXmlModel.FromUserName, WxXmlModel.ToUserName, "/:rose农场大数据欢迎你!/:rose");

  }
  break;
  case "event":
  switch (WxXmlModel.Event.ToLower())
  {
   case "subscribe":
   if (string.IsNullOrEmpty(WxXmlModel.EventKey))
   {
    XML = ResponseMessage.ReText(WxXmlModel.FromUserName, WxXmlModel.ToUserName, "关注成功!/:rose");

   }
   else
   {
    XML = ResponseMessage.SubScanQrcode(WxXmlModel.FromUserName, WxXmlModel.ToUserName, WxXmlModel.EventKey);//扫描带参数二维码先关注后推送事件
   }
   break;
   case "scan":
   XML = ResponseMessage.ScanQrcode(WxXmlModel.FromUserName, WxXmlModel.ToUserName, WxXmlModel.EventKey);//扫描带参数二维码已关注 直接推送事件
   break;
   case "click"://处理单击事件
   if (WxXmlModel.EventKey == "p1")
   {
    //自己的业务逻辑
   }
   else
   {
    //自己的业务逻辑
   }
   break;
   case "unsubscribe"://取消关注
   break;
  }
  break;
  default://默认回复
  break;
 }
 Response.Write(XML);//输出组织的XML信息

 }

This is the information processing of the menu. People who don’t know the truth seem to ask what the so-called ResponseMessage means. OK, I can no longer complain about the WeChat public platform I have researched in the past three days.


public class ResponseMessage

{

 #region 接收的类型
 /// 
 /// 接收文本
 /// 
 /// 
 /// 
 /// 
 /// 
 public static string GetTextTest(string FromUserName, string ToUserName, string Content, string key)
 {
 CommonMethod.WriteTxt(Content);//接收的文本消息
 string XML = "";
 switch (Content)
 {
  case "关键字":
  XML = ReText(FromUserName, ToUserName, "关键词回复测试——兴农丰华:" + key);
  break;
  case "单图文":
  XML = ReArticle(FromUserName, ToUserName, "测试标题", "测试详情——兴农丰华:" + key, "http://www.xnfhtech.com/templets/boze/images/20120130083143544.gif", "http://www.xnfhtech.com/");
  break;
  default:
  XML = ReText(FromUserName, ToUserName, "无对应关键字——兴农丰华:" + key);
  break;
 }
 return XML;

 }

 /// 
 /// 未关注扫描带参数二维码
 /// 
 /// 
 /// 
 /// 
 /// 
 public static string SubScanQrcode(string FromUserName, string ToUserName, string EventKey)
 {
 return "";
 }
 

 /// 
 /// 已关注扫描带参数二维码
 /// 
 /// 
 /// 
 /// 
 /// 
 public static string ScanQrcode(string FromUserName, string ToUserName, string EventKey)
 {
 return "";
 }
 #endregion
 

 #region 回复方式
 /// 
 /// 回复文本
 /// 
 /// 发送给谁(openid)
 /// 来自谁(公众账号ID)
 /// 回复类型文本
 /// 拼凑的XML

 public static string ReText(string FromUserName, string ToUserName, string Content)
 {
 string XML = "";//发送给谁(openid),来自谁(公众账号ID)
 XML += "" + CommonMethod.ConvertDateTimeInt(DateTime.Now) + "";//回复时间戳
 XML += "";//回复类型文本
 XML += "0";//回复内容 FuncFlag设置为1的时候,自动星标刚才接收到的消息,适合活动统计使用
 return XML;

 }
 

 /// 
 /// 回复单图文
 /// 
 /// 发送给谁(openid)
 /// 来自谁(公众账号ID)
 /// 标题
 /// 详情
 /// 图片地址
 /// 地址
 /// 拼凑的XML

 public static string ReArticle(string FromUserName, string ToUserName, string Title, string Description, string PicUrl, string Url)
 {

 string XML = "";//发送给谁(openid),来自谁(公众账号ID)
 XML += "" + CommonMethod.ConvertDateTimeInt(DateTime.Now) + "";//回复时间戳
 XML += "1";
 XML += "<![CDATA[" + Title + "]]>";
 XML += "0";
 return XML;

 }


 /// 
 /// 多图文回复
 /// 
 /// 发送给谁(openid)
 /// 来自谁(公众账号ID)
 /// 图文数量
 /// 
 /// 

 public static string ReArticle(string FromUserName, string ToUserName, int ArticleCount, System.Data.DataTable dtArticle)
 {

 string XML = "";//发送给谁(openid),来自谁(公众账号ID)
 XML += "" + CommonMethod.ConvertDateTimeInt(DateTime.Now) + "";//回复时间戳
 XML += "" + ArticleCount + "";
 foreach (System.Data.DataRow Item in dtArticle.Rows)

 {
  XML += "<![CDATA[" + Item["Title"] + "]]>";
 }
 XML += "0";
 return XML;

 }

 #endregion

 }

OK, with your own logic code, is the reply perfectly implemented?

The seventh pit, I really don’t want to count anymore, are you sure this reply is okay? To be honest, I'm not sure, because after you write it, do you know where to call it? My dear, damn, it's safest to add the reply after the server verification is passed. I have no morals anymore.

What should we say next? Let’s talk about obtaining user information, because these things of ours are generally based on H5 pages. Therefore, we have to use the

Summary of five pitfalls encountered in PHP WeChat public account development

thing we configured before. In fact, compared to the previous one, this one has at least a lot less pitfalls. Sincerely, baby will just temporarily Not to mention he was cheated. Let’s put some code.


//微信网页授权2.0
public class Oauth2
{
 JavaScriptSerializer Jss = new JavaScriptSerializer();
 public Oauth2() { }
 

 /// 
 /// 对页面是否要用授权
 /// 
 /// 微信应用id
 /// 回调页面
 /// 应用授权作用域snsapi_userinfo(不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且,即使在未关注的情况下,只要用户授权,也能获取其信息)
 /// 授权地址

 public string GetCodeUrl(string Appid, string redirect_uri, string scope)
 {
 return string.Format("https://open.weixin.qq.com/connect/oauth2/authorize?appid={0}&redirect_uri={1}&response_type=code&scope={2}&state=STATE#wechat_redirect", Appid, redirect_uri, scope);
 }

 

 /// 
 /// 对页面是否要用授权
 /// 
 /// 微信应用id
 /// 回调页面
 /// 应用授权作用域snsapi_userinfo(不弹出授权页面,直接跳转,只能获取用户openid),snsapi_userinfo (弹出授权页面,可通过openid拿到昵称、性别、所在地。并且,即使在未关注的情况下,只要用户授权,也能获取其信息)
 /// 授权地址
 public string GetCodeUrl(string Appid, string redirect_uri, string scope,string state)
 {
 return string.Format("https://open.weixin.qq.com/connect/oauth2/authorize?appid={0}&redirect_uri={1}&response_type=code&scope={2}&state={3}#wechat_redirect", Appid, redirect_uri, scope, state);
 }

 

 /// 
 /// 用code换取openid 此方法一般是不获取用户昵称时候使用
 /// 
 /// 
 /// 
 /// 回调页面带的code参数
 /// 微信用户唯一标识openid
 public string CodeGetOpenid(string Appid, string Appsecret, string Code)
 {
 string url = string.Format("https://api.weixin.qq.com/sns/oauth2/access_token?appid={0}&secret={1}&code={2}&grant_type=authorization_code", Appid, Appsecret, Code);
 string ReText = CommonMethod.WebRequestPostOrGet(url, "");//post/get方法获取信息
 Dictionary DicText = (Dictionary)Jss.DeserializeObject(ReText);
 if (!DicText.ContainsKey("openid"))
  return "";
 return DicText["openid"].ToString();
 }

 

 /// 
 ///用code换取获取用户信息(包括非关注用户的)
 /// 
 /// 
 /// 
 /// 回调页面带的code参数
 /// 获取用户信息(json格式)

 public string GetUserInfo(string Appid, string Appsecret, string Code)
 {

 string url = string.Format("https://api.weixin.qq.com/sns/oauth2/access_token?appid={0}&secret={1}&code={2}&grant_type=authorization_code", Appid, Appsecret, Code);
 string ReText = CommonMethod.WebRequestPostOrGet(url, "");//post/get方法获取信息
 Dictionary DicText = (Dictionary)Jss.DeserializeObject(ReText);
 if (!DicText.ContainsKey("openid"))

 {

  log.Error("获取openid失败,错误码:" + DicText["errcode"].ToString());

 return "";

 }
 else

 {

  return CommonMethod.WebRequestPostOrGet("https://api.weixin.qq.com/sns/userinfo?access_token=" + DicText["access_token"] + "&openid=" + DicText["openid"] + "&lang=zh_CN", "");

 }

 }

 

 

 /// 
 /// 通过openId获取用户信息
 /// 
 /// 
 /// 
 /// 
 public string GetUserInfo(string accesstoken, string openid)

 {
 string url = string.Format("https://api.weixin.qq.com/cgi-bin/user/info?access_token={0}&openid={1}&lang=zh_CN", accesstoken, openid);
 return CommonMethod.WebRequestPostOrGet(url, "");//post/get方法获取信息

 }

}

When we need to call, we can directly use the method inside to obtain the WeChat webpage authorization. For example, we need to obtain authorization for the B view under the A controller, and obtain User-related information, then we can call it directly, such as GetCodeUrl(appid, "http://" + Url + "/A/B", "snsapi_userinfo")

Here I am still Let’s complain.

The eighth pit, WeChat menu JSON url splicing, isn’t there js verification in front, so, what the heck, just add http:/ /.

But after authorization here, because we need to use a lot of user information, this is the problem of value transfer on the H5 page. I use Session in the project and write a public method directly. If If Session has a value, it is taken directly. Regarding some of the stuff inside, I would like to explain that not all the code needs to be posted. The code here is just what I personally think needs to be posted. So there may be some methods that you can’t see. If necessary, you can leave a message on this page. Thank you.


public string getSession()

{

 log.Error("GetSession");
 string oauthStr = "";
 try

 {
 if (Session != null && (Session["oauthStr"] == null || string.IsNullOrEmpty(Session["oauthStr"].ToString())))
 {
  if (!string.IsNullOrEmpty(Request.QueryString["code"]))
  {
  Oauth2 oauth = new Oauth2();
  string code = Convert.ToString(Request["code"]);
  oauthStr = oauth.GetUserInfo(ConfigurationManager.AppSettings["AppID"],
   ConfigurationManager.AppSettings["AppSecret"], code);
   Session["oauthStr"] = oauthStr;
  Tools.WAEntity.OAuthUser oAuthUser = new Tools.WAEntity.OAuthUser();
  oAuthUser = Tools.JsonHelper.ParseFromJson(oauthStr);
  }
  return oauthStr;

 }
 else

 {

  Tools.WAEntity.OAuthUser oAuthUser = new Tools.WAEntity.OAuthUser();
  oAuthUser = Tools.JsonHelper.ParseFromJson(Session["oauthStr"].ToString());
  return Session["oauthStr"].ToString();

 }

 }

 catch (Exception e) { log.Error(e.ToString()); return oauthStr; };

}

Then every time I encounter a page that needs to obtain information, I usually just call this.

Basically the rest is the business logic that we have to deal with ourselves, let’s continue talking about the pitfalls.

The ninth pit, uploading pictures on WeChat, is definitely not the only one who is pitted. I really believe in this baby, whether you believe it or not. Special pictures cannot be uploaded in a for loop. Of course, this is only for Apple models, there is still no problem with Android.
I mentioned the issue of JS security verification earlier. Here we call these verifications, request some appropriate permissions, and then obtain image information and so on.

Summary of five pitfalls encountered in PHP WeChat public account development

#Don’t worry, the baby is talking about the picture above, not the little brother in the picture. . . . .

Let’s continue to come back and look at the code.

Let’s first process Json


public class JsApi

{
 JavaScriptSerializer Jss = new JavaScriptSerializer(); 

 public JsApi() { } 
 const string URL_FORMAT_TICKET = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={0}&type=jsapi";

 #region 验证JsApi权限配置
 /// 
 /// 获取JsApi权限配置的数组/四个参数
 /// 
 /// 应用id
 /// 密钥
 /// json格式的四个参数
 public string GetJsApiInfo(string Appid, string Appsecret)
 {
 string jsapi_ticket = "";

 //ticket 缓存7200秒

 if (System.Web.HttpContext.Current.Session["jsapi_ticket"] == null)

 {
  string ticketurl = string.Format(URL_FORMAT_TICKET, BasicApi.GetAccessToken(Appid, Appsecret));//"https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=" + GetAccessToken(Appid, Appsecret) + "&type=jsapi"
  jsapi_ticket = CommonMethod.WebRequestPostOrGet(ticketurl, "");//BasicApi.GetTokenSession
  System.Web.HttpContext.Current.Session["jsapi_ticket"] = jsapi_ticket;
  System.Web.HttpContext.Current.Session.Timeout = 7200;
  BasicApi.WriteTxt("jsapi_ticket1:" + jsapi_ticket);


 }
 else
 {
  jsapi_ticket = System.Web.HttpContext.Current.Session["jsapi_ticket"].ToString();
  BasicApi.WriteTxt("jsapi_ticket2:" + jsapi_ticket);
 } 

 Dictionary respDic = (Dictionary)Jss.DeserializeObject(jsapi_ticket);
 jsapi_ticket = respDic["ticket"].ToString();//获取ticket
 string timestamp = CommonMethod.ConvertDateTimeInt(DateTime.Now).ToString();//生成签名的时间戳
 string nonceStr = CommonMethod.GetRandCode(16);//生成签名的随机串
 string url = System.Web.HttpContext.Current.Request.Url.AbsoluteUri.ToString();//当前的地址
 BasicApi.WriteTxt("url:" + url);
 string[] ArrayList = { "jsapi_ticket=" + jsapi_ticket, "timestamp=" + timestamp, "noncestr=" + nonceStr, "url=" + url };
 Array.Sort(ArrayList);
 string signature = string.Join("&", ArrayList);
 signature = FormsAuthentication.HashPasswordForStoringInConfigFile(signature, "SHA1").ToLower();
 string r = "{\"appId\":\"" + Appid + "\",\"timestamp\":" + timestamp + ",\"nonceStr\":\"" + nonceStr +
   "\",\"signature\":\"" + signature +
   "\",\"jsApiList\":[\"chooseImage\",\"previewImage\",\"uploadImage\",\"downloadImage\",\"scanQRCode\",\"onMenuShareQQ\"]}";
 BasicApi.WriteTxt("r:" + r.Replace(" ", ""));
 return r.Replace(" ", "");

 }

}

and then look at the specific calls.

The background code is actually very simple, just output the configuration file directly, and then directly call the front-end js.


##

JsApi jsApi = new JsApi();
string config = jsApi.GetJsApiInfo(appId, appSecret);
ViewBag.config = config;

前台代码,其实也不难,这个有官方的例子的。 


OK,后台方法其实也很简单,就是一个二进制文件处理,不对,简单个蛋蛋,特么的,因为路径的问题,坑了宝宝一个小时,特么的。还有这里建议,等微信图片下载完成之后再给前台加载图片,保证每一个图片都加载完成,保证后台的图片的上传完成。 


/// 
/// 下载多媒体文件
/// 
/// 公众号
/// 媒体ID
/// 返回下载是否成功
/// 添加的图片类型
/// 返回多媒体文件数据;如果下载失败,返回null。
public JsonResult DownloadWxPhoto(string mediaId, int types)

{

 ErrorMessage errorMessage;
 string access_token = BasicApi.GetAccessToken(ConfigurationManager.AppSettings["AppID"], ConfigurationManager.AppSettings["AppSecret"]);
 byte[] data = MediaHelper.Download(access_token, mediaId, out errorMessage);
 string files = String.Empty, fileName = String.Empty;
 files = Server.MapPath("~/Wxinphoto/");
 if (!Directory.Exists(files))
 {
 Directory.CreateDirectory(files);
 }
 fileName = files + DateTime.Now.Ticks + ".jpg";
 if (data != null)
 {
 bool flag = writeFile(data, fileName);
 if (flag)
 {
  errorMessage = new ErrorMessage(ErrorMessage.SuccessCode, "下载多媒体文件成功。");
 }
 else
 {
  errorMessage = new ErrorMessage(ErrorMessage.ExceptionCode, "从微信服务器下载多媒体文件失败。");
 }

 }
 else
 errorMessage = new ErrorMessage(ErrorMessage.ExceptionCode, "从微信服务器下载多媒体文件失败。");
 return Json(new { result = "/" + urlconvertor(fileName), errorMessage = errorMessage });

}


//读filename到byte[] 
private byte[] ReadFile(string fileName)
{
 FileStream pFileStream = null;
 byte[] pReadByte = new byte[0];
 try
 {
 pFileStream = new FileStream(fileName, FileMode.Open, FileAccess.Read);
 BinaryReader r = new BinaryReader(pFileStream);
 r.BaseStream.Seek(0, SeekOrigin.Begin); //将文件指针设置到文件开
 pReadByte = r.ReadBytes((int)r.BaseStream.Length);
 return pReadByte;
 }
 catch
 {
 return pReadByte;
 }
 finally
 {
 if (pFileStream != null)
  pFileStream.Close();

 }

}

 

//写byte[]到fileName

private bool writeFile(byte[] pReadByte, string fileName)

{

 FileStream pFileStream = null;

 try

 {
 pFileStream = new FileStream(fileName, FileMode.OpenOrCreate);
 pFileStream.Write(pReadByte, 0, pReadByte.Length);
 }
 catch

 {
 return false;
 }
 finally

 {
 if (pFileStream != null)

  pFileStream.Close();

 }

 return true;

}
/// 
/// 判断目标字节数组是否位于源字节数组的开始
/// 
/// 源字节数组
/// 目标字节数组
/// 返回目标字节数组是否位于源字节数组的开始

private bool StartsWithBytes(byte[] source, byte[] target)

{

 if (source == null && target == null)

 return true;

 if (source == null && target != null || source != null && target == null)
 return false;
 if (source.Length < target.Length)
 return false;
 bool startsWith = true;
 for (int i = 0; i < target.Length; i++)
 {
 if (source[i] != target[i])

 {
  startsWith = false;

  break;

 }

 }

 return startsWith;

}

 是不是以为这就算完事了,我的乖乖,头像上传了,微信摄像头也特么该调用的调用了,宝宝好幸福,宝宝也是牛人一个了,记住前面的东东,宝宝还没有说坑呢。
来重复我们的第九个坑,特么的,你JS写个for循环要是能循环把图片上传到后台,宝宝也服气,真的,宝宝服气。 

直接说吧,最后我自己想了下,也和队友讨论了下,可能是因为微信有什么验证,导致之后一张图片上传成功之后,才能进行一张,但是我们Iphone就是特么的特例,大Android没用问题的,就是Iphone有了问题,而且问题不小,上传四张图片吧,老特么是最后一张,最后,找到了万能的网友,感谢你,不过宝宝已经忘记了在哪里找到的了,尴尬了。。。。。。。。。。。 


请记住,递归就特么可以了。

说到这里,宝宝已经不想多说什么了,特么的产品你能不能不装逼,你特么见过那个微信能回复一个信息直接跳转网页的,你咋不去屎呢,联想到前几天大阿里的月饼时间,突然感觉我们程序员挺悲剧的,成功的都是特么的产品,然后出问题的都是我们程序员的锅?试问一下,这个锅真心我们程序员该背么。 

算了,还是不吐槽了,已经无力了。。。。宝宝92年降临,现在确实82年的皮肤呀,唉,宝宝累了,真的。 

顺便给点H5页面的建议吧。比如当点击返回键的时候,我们需要刷新页面的时候,就是所谓的判断页面要不要刷新,这里有很多种方法,但是微信里面宝宝还是觉得这么干靠谱。 


还有,那个微信执行完成之后想直接退出当前界面进入微信公众号界面的,直接调用微信的一个内置的方法即可。记得写到里面。 

WeixinJSBridge.call('closeWindow'); //这是微信关闭当前网页

这么自信的以为自己搞定了所有,你跑呀,你要跑起来,嗯哼,别不服气。 

微信公众账号指第十坑,我自己加的,哈哈,就是前面的JS验证的时候,你不要头文件,怎么搞定这些事情,哈哈。是不是宝宝赢了。Oh  perfect,I like it。

The above is the detailed content of Summary of five pitfalls encountered in PHP WeChat public account development. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn