小程序客服消息处理
三种消息类型:
// 文本消息
{
"signature": "ee6e400de7972484ec6f3014c2f77504925a4707",
"timestamp": "1552298008",
"nonce": "540004383",
"openid": "oPzrj5EAP25lzOVW6qa0m8MUlLXA",
"encrypt_type": "aes",
"msg_signature": "74c419155e49cefab8ab1dbd440b9389acc47e2a",
"URL": "http:\/\/qiang.lt.ngapp.net\/wechat\/noti/fy",
"ToUserName": "zhaishuaigan",
"FromUserName": "oPzrj5EAP25lzOVW6qa0m8MUlLXA",
"CreateTime": 1552293166,
"MsgType": "text",
"Content": "hello",
"MsgId": 1,
"Encrypt": "PloOW0ucj9SkMpMC...."
}
// 事件消息
{
"signature": "3e5860da822266b9f03f8d5380615f9be0ae2db7",
"timestamp": "1552301914",
"nonce": "693467019",
"openid": "oPzrj5EAP25lzOVW6qa0m8MUlLXA",
"encrypt_type": "aes",
"msg_signature": "eda0cdd567b35df4501ee2041e0d191db81798f2",
"ToUserName": "gh_cc23a0a84984",
"FromUserName": "oPzrj5EAP25lzOVW6qa0m8MUlLXA",
"CreateTime": 1552301914,
"MsgType": "event",
"Event": "user_enter_tempsession",
"SessionFrom": "open-type='contact'",
"Encrypt": "oMP\/xo2RdcbK07vvoxxxx....."
}
// 图片消息
{
"signature": "ebd79f0c44f01dc26ef83ffeeadbdb3af5b960da",
"timestamp": "1552302190",
"nonce": "1322948029",
"openid": "oPzrj5EAP25lzOVW6qa0m8MUlLXA",
"encrypt_type": "aes",
"msg_signature": "06613ca44e189d95140166898f80a27ab10baa80",
"ToUserName": "gh_cc23a0a84984",
"FromUserName": "oPzrj5EAP25lzOVW6qa0m8MUlLXA",
"CreateTime": 1552302190,
"MsgType": "image",
"PicUrl": "http:\/\/mmbiz.qpic.cn\/mmbiz_jpg\/WwPGxFUiaTRZ6zVvTTGVbhfa9Vic801ux7OHobAAiaD3xRMYeJzbic4ISvwn736dpK0OMTlaYvX7GoTRgE8LqObkIQ\/0",
"MsgId": 22223624286040849,
"MediaId": "3WiHLigPtkJtqoLvPD2HgPsAtb3vzDvTWyo0sP5s-dshtS7oZOlCW7c3RuD1nwBt",
"Encrypt": "gictSCCy+Zxxxx...."
}
ThinkPHP 5.1保存消息代码
$request = request()->param();
$data = [
'signature' => $request['signature'],
'timestamp' => $request['timestamp'],
'nonce' => $request['nonce'],
'openid' => $request['openid'],
'encrypt_type' => $request['encrypt_type'],
'msg_signature' => $request['msg_signature'],
'to_username' => $request['ToUserName'],
'from_username' => $request['FromUserName'],
'msg_type' => $request['MsgType'],
'msg_id' => isset($request['MsgId']) ? $request['MsgId'] : '',
'encrypt' => isset($request['Encrypt']) ? $request['Encrypt'] : '',
];
$msgInfo = [];
switch ($request['MsgType']) {
case 'text':
$msgInfo['text'] = $request['Content'];
break;
case 'image':
$msgInfo['pic'] = $request['PicUrl'];
$msgInfo['media_id'] = $request['MediaId'];
break;
case 'event':
$msgInfo['event'] = $request['Event'];
$msgInfo['session_from'] = $request['SessionFrom'];
break;
}
$data['msg_info'] = json_encode($msgInfo);
CustomerMessage::create($data);
return '';
数据库表结构设计
<?php
use think\migration\Migrator;
use think\migration\db\Column;
class CreateCustomerMessageTable extends Migrator
{
public function change()
{
$this->table('customer_message')
->addColumn(Column::string('signature')
->setDefault('')
->setComment('签名'))
->addColumn(Column::integer('timestamp')
->setDefault(0)
->setComment('时间戳'))
->addColumn(Column::string('nonce')
->setDefault('')
->setComment('随机数'))
->addColumn(Column::string('openid')
->setDefault('')
->setComment('客户id'))
->addColumn(Column::string('encrypt_type')
->setDefault('')
->setComment('加密方式'))
->addColumn(Column::string('msg_signature')
->setDefault('')
->setComment('消息签名'))
->addColumn(Column::string('to_username')
->setDefault('')
->setComment('消息接收者'))
->addColumn(Column::string('from_username')
->setDefault('')
->setComment('消息来源'))
->addColumn(Column::string('msg_type')
->setDefault('')
->setComment('消息类型'))
->addColumn(Column::text('msg_info')
->setNull(true)
->setComment('消息内容'))
->addColumn(Column::string('msg_id')
->setDefault('')
->setComment('消息id'))
->addColumn(Column::text('encrypt')
->setNull(true)
->setComment('加密数据'))
->addColumn(Column::dateTime('create_time')
->setDefault('CURRENT_TIMESTAMP')
->setComment('创建时间'))
->addColumn(Column::dateTime('update_time')
->setDefault('CURRENT_TIMESTAMP')
->setComment('更新时间'))
->addColumn(Column::dateTime('delete_time')
->setNull(true)
->setComment('删除时间'))
->create();
}
}
关于input file的文件过滤方法
限制只能选择图片: <input type="file" accept="image/*" />
限制只能选择视频: <input type="file" accept="video/*" />
限制只能选择音频: <input type="file" accept="audio/*" />
直接打开摄像头拍照: <input type="file" accept="image/*" capture="camera" />
直接打开摄像头录像: <input type="file" accept="video/*" capture="camera" />
一个markdown编辑器editor.md
Editor.md
Editor.md : The open source embeddable online markdown editor (component), based on CodeMirror & jQuery & Marked.
Features
- Support Standard Markdown / CommonMark and GFM (GitHub Flavored Markdown);
- Full-featured: Real-time Preview, Image (cross-domain) upload, Preformatted text/Code blocks/Tables insert, Code fold, Search replace, Read only, Themes, Multi-languages, L18n, HTML entities, Code syntax highlighting…;
- Markdown Extras : Support ToC (Table of Contents), Emoji, Task lists, @links.html"">@Links…;
- Compatible with all major browsers (IE8+), compatible Zepto.js and iPad;
- Support decode & fliter of the HTML tags & attributes;
- Support TeX (LaTeX expressions, Based on KaTeX), Flowchart and Sequence Diagram of Markdown extended syntax;
- Support AMD/CMD (Require.js & Sea.js) Module Loader, and Custom/define editor plugins;
Editor.md 是一款开源的、可嵌入的 Markdown 在线编辑器(组件),基于 CodeMirror、jQuery 和 Marked 构建。
主要特性
- 支持通用 Markdown / CommonMark 和 GFM (GitHub Flavored Markdown) 风格的语法,也可变身为代码编辑器;
- 支持实时预览、图片(跨域)上传、预格式文本/代码/表格插入、代码折叠、跳转到行、搜索替换、只读模式、自定义样式主题和多语言语法高亮等功能;
- 支持 ToC(Table of Contents)、Emoji表情、Task lists、@links.html">@链接等 Markdown 扩展语法;
- 支持 TeX 科学公式(基于 KaTeX)、流程图 Flowchart 和 时序图 Sequence Diagram;
- 支持识别和解析 HTML 标签,并且支持自定义过滤标签及属性解析,具有可靠的安全性和几乎无限的扩展性;
- 支持 AMD / CMD 模块化加载(支持 Require.js & Sea.js),并且支持自定义扩展插件;
- 兼容主流的浏览器(IE8+)和 Zepto.js,且支持 iPad 等平板设备;
Examples
https://pandao.github.io/editor.md/examples/index.html
Download & install
Bower install :
bower install editor.md
Usages
HTML:
```html
cms想法
关于字体图标
后台模块或按钮图标采用 http://www.iconfont.cn/ 的图标,
智能解析iconfont.css提取所有图标
关于模块
新建模块需要设置:
模块标识
模块名称
模块字段 (标识, 名称, 类型, 默认值, 新增验证函数, 更新验证函数)
// 默认值可读取配置
// 验证函数自动读取验证辅助类的方法和文档
列表显示字段
// 字段显示有格式化方式, 自动读取格式化辅助类和文档
详情页显示字段
添加数据界面显示字段
编辑页面显示字段
排序字段和方式(asc|desc|自定义)
搜索字段
删除数据方式 (软删除, 直接删除)
验证辅助类
设计思想
class Validate {
public function username($value, $msg, $params...) {
}
}
分两个, 一个系统, 一个用户自定义
想到的辅助方法:
用户名,密码,邮箱,手机号,不能为空,长度限制,只允许英文,正则
通过获取类的注释, 在需要的地方列出验证方式列表
字段和数据格式化辅助类
设计思想
class Format{
public function toImgTag($value, $params....) {
}
}
分两个, 一个系统, 一个用户自定义
想到的辅助方法:
时间,状态,图片,关联表字段 (表名, 字段名, 分隔符)
通过获取类的注释, 在需要的地方列出格式化辅助方法列表
php内置服务器使用方法
1 启动Web服务器
$ cd ~/public_html
$ php -S localhost:8000
终端输出信息:
PHP 5.4.0 Development Server started at Thu Jul 21 10:43:28 2011
Listening on localhost:8000
Document root is /home/me/public_html
Press Ctrl-C to quit
当请求了 http://localhost:8000/ 和 http://localhost:8000/myscript.html 地址后,终端输出类似如下的信息:
PHP 5.4.0 Development Server started at Thu Jul 21 10:43:28 2011
Listening on localhost:8000
Document root is /home/me/public_html
Press Ctrl-C to quit.
[Thu Jul 21 10:48:48 2011] ::1:39144 GET /favicon.ico - Request read
[Thu Jul 21 10:48:50 2011] ::1:39146 GET / - Request read
[Thu Jul 21 10:48:50 2011] ::1:39147 GET /favicon.ico - Request read
[Thu Jul 21 10:48:52 2011] ::1:39148 GET /myscript.html - Request read
[Thu Jul 21 10:48:52 2011] ::1:39149 GET /favicon.ico - Request read
2 启动web服务器时指定文档的根目录
$ cd ~/public_html
$ php -S localhost:8000 -t foo/
终端显示信息:
PHP 5.4.0 Development Server started at Thu Jul 21 10:50:26 2011
Listening on localhost:8000
Document root is /home/me/public_html/foo
Press Ctrl-C to quit
如果你在启动命令行后面附加一个php脚本文件,那这个文件将会被当成一个“路由器”脚本。这个脚本将负责所有的HTTP请求,如果这个脚本执行时返回FALSE,则被请求的资源会正常的返回。如果不是FALSE,浏览里显示的将会是这个脚本产生的内容。
3 使用路由器脚本
在这个例子中,对图片的请求会返回相应的图片,但对HTML文件的请求会显示“Welcome to PHP”:
<?php
// router.php
if (preg_match('/\.(?:png|jpg|jpeg|gif)$/', $_SERVER["REQUEST_URI"])) {
return false; // serve the requested resource as-is.
} else {
echo "<p>Welcome to PHP</p>";
}
?>
执行:
$ php -S localhost:8000 router.php
4 判断是否是在使用内置web服务器
通过程序判断来调整同一个PHP路由器脚本在内置Web服务器中和在生产服务器中的不同行为:
<?php
// router.php
if (php_sapi_name() == 'cli-server') {
/* route static assets and return false */
}
/* go on with normal index.php operations */
?>
执行:
$ php -S localhost:8000 router.php
这个内置的web服务器能识别一些标准的MIME类型资源,它们的扩展有:.css, .gif, .htm, .html, .jpe, .jpeg, .jpg, .js, .png, .svg, and .txt。对.htm 和 .svg 扩展到支持是在PHP 5.4.4之后才支持的。
5 处理不支持的文件类型
如果你希望这个Web服务器能够正确的处理不被支持的MIME文件类型,这样做:
<?php
// router.php
$path = pathinfo($_SERVER["SCRIPT_FILENAME"]);
if ($path["extension"] == "ogg") {
header("Content-Type: video/ogg");
readfile($_SERVER["SCRIPT_FILENAME"]);
}
else {
return FALSE;
}
?>
执行:
$ php -S localhost:8000 router.php
如果你希望能远程的访问这个内置的web服务器,你的启动命令需要改成下面这样:
6 远程访问这个内置Web服务器
$ php -S 0.0.0.0:8000
这样你就可以通过 8000 端口远程的访问这个内置的web服务器了。
写了一个ThinkPHP的模板引擎, 仿angular的, 简单版
前段时间学习angularjs, 里面的模板思想和实现方法很酷, 就心血来潮, 想实现一个php版的, 今天试着写了一下, 发现貌似可以, 具体看源码.
./ThinkPHP/Library/Think/Template/Driver/Angular.class.php
<?php
namespace Think\Template\Driver;
use Think\Storage;
/**
* Angular模板引擎驱动
*/
class Angular {
private $config = array();
private $tpl_var = array();
/**
* 架构函数
*/
public function __construct() {
$this->config['cache_path'] = C('CACHE_PATH');
$this->config['tpl_dir'] = THEME_PATH;
$this->config['cache_path'] = C('CACHE_PATH');
$this->config['template_suffix'] = C('TMPL_TEMPLATE_SUFFIX');
$this->config['cache_suffix'] = C('TMPL_CACHFILE_SUFFIX');
$this->config['tmpl_cache'] = C('TMPL_CACHE_ON');
$this->config['cache_time'] = C('TMPL_CACHE_TIME');
$this->config['attr'] = 'tp-';
}
/**
* 编译模板
* @param type $tpl_file 模板文件
* @param type $tpl_var 模板变量
*/
public function fetch($tpl_file, $tpl_var) {
$this->tpl_var = $tpl_var;
$tpl_file = $this->load_template($tpl_file);
Storage::load($tpl_file, $tpl_var, null, 'tpl');
}
/**
* 加载主模板并缓存
* @param string $tpl_file 模板文件名
* @return string 缓存的模板文件名
*/
public function load_template($tpl_file) {
if (is_file($tpl_file)) {
// 读取模板文件内容
$tpl_content = file_get_contents($tpl_file);
} else {
$tpl_content = $tpl_file;
}
// 根据模版文件名定位缓存文件
$tpl_cache_file = $this->config['cache_path'] . md5($tpl_file) . $this->config['cache_suffix'];
if (Storage::has($tpl_cache_file) && !APP_DEBUG && $this->config['tmpl_cache']) {
return $tpl_cache_file;
}
// 编译模板内容
$tpl_content = $this->compiler($tpl_content);
Storage::put($tpl_cache_file, trim($tpl_content), 'tpl');
return $tpl_cache_file;
}
/**
* 编译模板内容
* @param string $tpl_content 模板内容
* @return string 编译后端php混编代码
*/
protected function compiler($tpl_content) {
//模板解析
$tpl_content = $this->parse($tpl_content);
// 添加安全代码
$tpl_content = '<?php if (!defined(\'THINK_PATH\')) exit();?>' . $tpl_content;
// 优化生成的php代码
$tpl_content = str_replace('?><?php', '', $tpl_content);
return strip_whitespace($tpl_content);
}
/**
* 解析模板标签属性
* @param string $content 要模板代码
* @return string 解析后的模板代码
*/
public function parse($content) {
while (true) {
$sub = $this->match($content);
if ($sub) {
$method = 'parse_' . $sub['attr'];
if (method_exists($this, $method)) {
$content = $this->$method($content, $sub);
} else {
E("模板属性" . $this->config['attr'] . $sub['attr'] . '没有对应的解析规则');
break;
}
} else {
break;
}
}
$content = $this->parse_value($content);
return $content;
}
/**
* 解析include属性
* @param string $content 源模板内容
* @param array $match 一个正则匹配结果集, 包含 html, value, attr
* @return string 解析后的模板内容
*/
private function parse_include($content, $match) {
$tpl_name = $match['value'];
if (substr($tpl_name, 0, 1) == '$') {
//支持加载变量文件名
$tpl_name = $this->get(substr($tpl_name, 1));
}
$array = explode(',', $tpl_name);
$parse_str = '';
foreach ($array as $tpl) {
if (empty($tpl))
continue;
if (false === strpos($tpl, $this->config['template_suffix'])) {
// 解析规则为 模块@主题/控制器/操作
$tpl = T($tpl);
}
// 获取模板文件内容
$parse_str .= file_get_contents($tpl);
}
return str_replace($match['html'], $parse_str, $content);
}
/**
* 解析if属性
* @param string $content 源模板内容
* @param array $match 一个正则匹配结果集, 包含 html, value, attr
* @return string 解析后的模板内容
*/
private function parse_if($content, $match) {
$new = "<?php if ({$match['value']}) { ?>";
$new .= str_replace($match['exp'], '', $match['html']);
$new .= '<?php } ?>';
return str_replace($match['html'], $new, $content);
}
/**
* 解析repeat属性
* @param string $content 源模板内容
* @param array $match 一个正则匹配结果集, 包含 html, value, attr
* @return string 解析后的模板内容
*/
private function parse_repeat($content, $match) {
$new = "<?php foreach ({$match['value']}) { ?>";
$new .= str_replace($match['exp'], '', $match['html']);
$new .= '<?php } ?>';
return str_replace($match['html'], $new, $content);
}
/**
* 解析show属性
* @param string $content 源模板内容
* @param array $match 一个正则匹配结果集, 包含 html, value, attr
* @return string 解析后的模板内容
*/
private function parse_show($content, $match) {
$new = "<?php if ({$match['value']}) { ?>";
$new .= str_replace($match['exp'], '', $match['html']);
$new .= '<?php } ?>';
return str_replace($match['html'], $new, $content);
}
/**
* 解析hide属性
* @param string $content 源模板内容
* @param array $match 一个正则匹配结果集, 包含 html, value, attr
* @return string 解析后的模板内容
*/
private function parse_hide($content, $match) {
$new = "<?php if (!({$match['value']})) { ?>";
$new .= str_replace($match['exp'], '', $match['html']);
$new .= '<?php } ?>';
return str_replace($match['html'], $new, $content);
}
/**
* 解析普通变量和函数{$title}{:function_name}
* @param string $content 源模板内容
* @return string 解析后的模板内容
*/
private function parse_value($content) {
$content = preg_replace('/\{(\$.*?)\}/', '<?php echo \1 ?>', $content);
$content = preg_replace('/\{\:(.*?)\}/', '<?php echo \1 ?>', $content);
return $content;
}
/**
* 获取第一个表达式
* @param string $content 要解析的模板内容
* @return array 一个匹配的标签数组
*/
private function match($content) {
$reg = '#<(?<tag>[\w]+)[^>]*?\s(?<exp>' . preg_quote($this->config['attr']) . '(?<attr>[\w]+)=([\'"])(?<value>[^\4]*?)\4)[^>]*>#s';
$match = null;
if (!preg_match($reg, $content, $match)) {
return null;
}
$sub = $match[0];
$tag = $match['tag'];
/* 如果是但标签, 就直接返回 */
if (substr($sub, -2) == '/>') {
$match['html'] = $match[0];
return $match;
}
/* 查找完整标签 */
$start_tag_len = strlen($tag) + 1; // <div
$end_tag_len = strlen($tag) + 3; // </div>
$start_tag_count = 0;
$content_len = strlen($content);
$pos = strpos($content, $sub);
$start_pos = $pos + strlen($sub);
while ($start_pos < $content_len) {
$is_start_tag = substr($content, $start_pos, $start_tag_len) == '<' . $tag;
$is_end_tag = substr($content, $start_pos, $end_tag_len) == "</$tag>";
if ($is_start_tag) {
$start_tag_count++;
}
if ($is_end_tag) {
$start_tag_count--;
}
if ($start_tag_count < 0) {
$match['html'] = substr($content, $pos, $start_pos - $pos + $end_tag_len);
return $match;
}
$start_pos++;
}
return null;
}
}
./Application/Home/Controller/TestController.class.php
<?php
namespace Home\Controller;
use Think\Controller;
class TestController extends Controller {
public function index() {
C('SHOW_PAGE_TRACE', true);
C('TMPL_ENGINE_TYPE', 'Angular');
$data = array();
$data['title'] = '标题';
$data['nav'] = array(
array('title' => '首页', 'url' => '/'),
array('title' => '文章', 'url' => '/article'),
array('title' => '图片', 'url' => '/pic'),
array('title' => '新闻', 'url' => '/news'),
);
$data['count'] = 6;
$data['list'] = array(
array('id' => 1, 'title' => '这是标题1', 'create_time' => strtotime('-5 seconds')),
array('id' => 2, 'title' => '这是标题2', 'create_time' => strtotime('-4 seconds')),
array('id' => 3, 'title' => '这是标题3', 'create_time' => strtotime('-3 seconds')),
array('id' => 4, 'title' => '这是标题4', 'create_time' => strtotime('-2 seconds')),
array('id' => 5, 'title' => '这是标题5', 'create_time' => strtotime('-1 seconds')),
array('id' => 6, 'title' => '这是标题6', 'create_time' => NOW_TIME),
);
$this->assign($data);
$this->display('index');
}
}
./Application/Home/View/Test/index.html
<!DOCTYPE html>
<html>
<head>
<title>Angular 模板测试 - {$title}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
* {
margin: 0px;
padding: 0px;
font-size: 12px;
color: #333;
line-height: 20px;
}
a {
color: #33F;
text-decoration: none;
}
a:hover {
color: #f00;
text-decoration: underline;
}
.center {
text-align: center;
}
h1{
font-size: 30px;
line-height: 50px;
}
.nav {
line-height: 30px;
}
.nav a{
padding: 0px;
margin: 0px 20px;
}
.main table{
width: 500px;
margin: 0px auto;
}
table {
border: 1px solid #666;
}
table td,
table th{
border: 1px solid #666;
line-height: 20px;
padding: 0px 5px;
}
table th{
background: #CCC;
}
#footer p{
text-align: center;
line-height: 30px;
}
</style>
</head>
<body>
<div class="header">
<h1 class="center">Angular 模板测试 - {$title}</h1>
<div class="nav center" tp-if="$nav">
<a tp-repeat="$nav as $vo" href="{$vo['url']}">{$vo['title']}</a>
</div>
</div>
<div class="main">
<table>
<tr>
<th>编号</th>
<th>标题</th>
<th>创建时间</th>
<th>操作</th>
</tr>
<tr tp-if="$list" tp-repeat="$list as $vo">
<td>{$vo['id']}</td>
<td>{$vo['title']}</td>
<td>{:date('Y-m-d H:i:s', $vo['create_time'])}</td>
<td><a href="#del={$vo['id']}">删除</a></td>
</tr>
<tr tp-if="$count">
<td colspan="4" class="center">共 {$count} 条数据</td>
</tr>
<tr tp-hide="$list">
<td colspan="4" class="center">没有数据</td>
</tr>
</table>
</div>
<div tp-include="footer"></div>
</body>
</html>
./Application/Home/View/Test/footer.html
<footer id="footer">
<div class="foot-warp">
<p>
© 2015 {:C('SITE_TITLE')} zhaishuaigan@qq.com 豫ICP备13012601号
</p>
</div>
</footer>
运行/Test/index, 显示结果
目前只是实现了简单的解析, 还需要进一步完善, 比如配置啊, 扩展更多的标签啊什么的.