D:\PanDownload\【微信小程序】\微信小程序商城构建全栈应用
第1章 前言:不同的时代,不同的Web
1-1 前言与导语
导语
好的课程需要包含俩方面:
一:整体的思路与编程思想(大局观,AOP ,10~20%)
二:具体的编程知识与技巧(TP5,小程序,数据库等80%)
books+code
1-2 产品所使用的技术
code+业务相结合
课程内容与产品技术点
ThinkPHP5 集成sql
编写业务逻辑 访问数据库 向客户端提供数据
面试简历细节专业
MySQL
数据存储 数据表设计 与业务紧密集合
微信api
支付 善于借鉴与模范,学习微信接口设计
小程序(公告号) 官网3>
直接与用户交互 体验很重要
微信支付宝淘宝百度
1-3 课程流程与体系
课程流程
一:服务端:
ThinkPHP5+MYSQL构建REST API
二:客户端:
向服务端请求数据,完成自身行为逻辑
三:CMS:
向服务端请求数据,实现发货与发送微信消息
总结下CMS的功能
俩大类
一.基础数据的增删改查,比如添加商品,删除商品类目
二.特殊操作,比如我们要实现的发送微信消息
分层
1-4 扩展课程:三端分离
重复学跳过学
不分离 结构不清晰
分离 扩展性 可用性差
独立开发
cms客服端 属于
1-5 项目特点
课程特点
我们想构建一个通用的.适合互联网公司的,有良好结构的产品
三段分离(客户端,服务端与数据管理分离)
基于REST API
API 多段分离
基于Token令牌管理权限
Token访问不同
一套架构适配ios android 小程序与单页面
真正理解MVC
AOP面向切面编程思想在真实项目中的应用
使用ORM的方式与数据库交互
Object Relational Mapping
MYSQL数据表设计与数据冗余的合理利用
qweb no oop
server oop
用面向对象的思维构建前端代码(ES6 class& Module)
1-6 TP5技术点简介
知识与技术
@TinkPHP5
Web框架三大核心知识(路由,控制器与模型)
验证器,读取器,缓存与全局异常处理
ORM:模型与关联模型
其他框架
Sequelize-node.js
SQLAlchemy 比tp5大 内容多
Introduction to Enitiy Framework
AOP思想通用
对象与对象关联处理 多表处理
1-7 微信技术点简介
@微信api
微信小程序
微信登录
微信支付(预订单,支付与回调通知处理)
微信支付(预订单,库存量检测与回调通知处理)
微信模板消息
微信公告平台 小程序
微信支付 登录
官网文档
模范借鉴api设计
1-8 MySql技术点简介
@MySQL
数据库表设计
数据沉余的合理利用
事务与锁在订单(库存量)检测中的应用
并发2方法 truefalsejava
零碎小知识点 文档数据其他get
1-9 学习方式
代码本身并不难,静下心学习就好
一关一关过,一级一级过,啃下这门课程
--难 -->容 反复
1-10 前置知识需求
PHP与面向对象的相关知识
ThinkPHP基本知识(对控制器和路由有一定的了解)
了解关系数据库(MySQL的基本使用,写过SQL语句)
小程序生命周期
开发api基础
模拟信息
小程序常用api
要有一个小程序帐号
1-11 扩展课程《理解Web与前端》
令牌tp缓存 文件 数据库
理解技术
语言与框架太多,关键是使用"它们"解决我们的问题
先有想法,再寻找"工具",而不是反过来
这个思想贯穿我们整个课程
泛化的Web.网站不是Web的全部,只是Web的一小部分
产品
Web产品矩阵
网站 IOS android 微信(H5 公告号) 小程序
每一项都离不开Web或Web技术
api公共开发
能写出代码和写出易维护的代码不是一回事儿
代码整洁维护分离
前端绝对不等于做界面和特效,前端与服务器在编程思维上的差异已经越来越小了
前端已经变的和服务器一样需要处理大量的业务逻辑
支持服务器业务的框架
1-12 扩展课程《第三方组件应该合理选择,特别是在学习阶段》
尽量不选择第三组件 diy简单
学成本钱找
课程内容与产品技术点
依赖或包管理
composer npm pip
php node python
php框架
Laravel
独立用插件 破坏sql项目结构少用
1-13 维护与提问
维护与提问
课程产品代码长期维护,请关注代码变更与升级
关于课程的相关问题,在慕课提问区提问
代码更新说明会发布在慕课手记,知乎专栏
小楼昨夜又秋风
https://www.baidu.com/link?url=GJ4U8IjUUUMA1tyxqXP34EyXgRaKXpgMzdbvOAwKl21b6EKCMtubFssRCIjlI9_F&wd=&eqid=cd7c099000035165000000065d970627
D:\PanDownload\【微信小程序】\微信小程序商城构建全栈应用
第2章 环境,工具与准备工作
2-1 环境与开发工具
Web框架
ThinkPHP 5.07
基础语言,环境
PHP 5.6
MySQL
Apache
XAMPP
客户端
小程序
开发工具
PHPStorm
微信Web开发者工具(VS Code)
PostMan
Navicat
2-2 安装xampp
环境与工具
首先安装tp5所需要的一些环境
ThinkPHP只是Web框架
还需要PHP运行环境
和Web服务器
常见Web服务器:Apache Nginx
ms apacheold nginxnew反向代理
https://lnmp.org/download.html
https://www.baidu.com/link?url=i_ZtQ_8MC8BDY9l7DulSqxeXVW8T0PueoR9NRlur9PJGy9wcsfdil38-BCvu_zCI_IqbnaBq39aqxfnrixtGOq&wd=&eqid=8c9b7ca30009eca3000000065d9b2116
进程pid kill
Apache Listen 80
php -v
https://www.xp.cn/
jsp
https://jingyan.baidu.com/article/37bce2beb6e2681002f3a20c.html
2-3 下载ThinkPHP5.07
ThinkPHP5
Composer安装
git安装
直接下载
应用项目 https://github.com/top-think/think
核心框架 https://github.com/top-think/framework
完整包含
2-4 三端命名与运行ThinkPHP5
项目独立命名
服务器程序:Zerg
客户端小程序:Protoss
CMS:Terran
Zerg-thinkphp5
thinkphp-thinkphp-fromwork
2-5 PHPStorm安装及常见使用技巧
wxtool 免登录
Fiddler
快捷键默认div
配置
addon 插件vim
如果是汉化后不能打开“设置”,原因是汉化包的原因,还原成默认的英文模式就可以了。
{
方法:
https://blog.csdn.net/weixin_37977916/article/details/89155066
1.下载 resources_zh_CN_IntelliJIDEA_2019.1_r1.jar
2.放入安装路径的 lib 目录中(例如:D:\PhpStorm 2019.1\lib)
3.重启phpstorm2019.1
ps:不需要删除原语言包。
}
search 显示行号 show line
切换代码 alt <- -> </->
查找文件,
alt+r error
ctril+shift+n所有文件 ctrl+E最近文件 ctrl+shift+E最近本地文件
set recent files 汉化没
ctrl+E
输入查看的东西
行注释ctrl+/
块注释ctrl+shift+/
alt+/ autoFill
搜索
全局ctrl+shift+f
局部ctrl+f
替换
全局ctrl+shift+r
局部ctrl+r
ALT+F1 选择当前文件或菜单中的任何视图工具栏
新建文件
目录focus
alt+insert
最左边
Structure
快速定位方法...
方法切换
alt+上下
--dug
Ctrl +反引号(`) 快速切换目前的配色/代码方案/快捷键方案/界面方案
https://www.cnblogs.com/kenshinobiy/p/6244707.html
2-6 TP5层次结构
tp5目录介绍 search
application 可以改 入口文件修
index 模块
controller 控制器
index.php 一个控制器
class->func index 操作 方法调用业务层(Module) 数据模型不view
extend 3类库
public外部站点目录
runtime 运行缓存日志文件
thinkphp核心框架
vendor 3插件
2-7 扩展:TP5自带的Web Server
调试可用
cd public
php -S locaolhost:8080 router.php
stop
ctrl=c
console git 托管
path +;shell -;cmd
2-8 在PHPstorm下断点调试代码
xdebug down phpstudy自带
echo
https://www.baidu.com/link?url=RJ6eeV-PCAqChcWAKVpl4KIxcjEgjqii8mOYLfosBDI-kwitj27Cpn1BVw5xMoqjaZe5lWo2xHSkpcSk8Kqkhq&wd=&eqid=efcb9997001b977f000000065d9ed778
https://xdebug.org/
phpinfo() 源代码-- xdebug 版本下
info.php search xdebug
修改php.ini
restart sever
图片-网
wxtool免登录* Fiddler* consoleGitsvn*
git/svn/cvs教程
https://blog.csdn.net/sinat_37812785/article/details/80243207汉化
https://blog.csdn.net/atsoar/article/details/80460440
https://www.runoob.com/svn/svn-tutorial.html
https://www.cnblogs.com/best/p/7474442.html
https://git-scm.com/download/win
xdebug_session_start=18631
该文件没有与之关联的应用来执行操作.
默认浏览器
不用加后面那个,直接点右上角的电话按钮打开,然后浏览器数据网址,phpstorm会自动捕获到断点的
可以正常调试,但是使用接口工具比如postman无法实现断点调试
调试快捷键f10...
waiting for incoming connection with ide key 18657
xdebug无法调 php.ini 参数没加 其他搜索用加
php.ini
{
[XDebug]
xdebug.profiler_output_dir="G:\phpStudy\tmp\xdebug"
xdebug.trace_output_dir="G:\phpStudy\tmp\xdebug"
zend_extension="G:\phpStudy\php\php-5.4.45\ext\php_xdebug.dll"
---下面追加才行---
xdebug.remote_enable=1
xdebug.remote_handler=dbgp
xdebug.remote_mode=req
xdebug.remote_host=localhost
xdebug.remote_port=9000
xdebug.idekey="PHPSIORM"
}
2-9 PATH_INFO URL路径模式解析
入口文件定义了
//定义应用目录
define('APP_PATH', DIR . '/../application/');
所以url不需要Application 直接模块
URL路径格式
http://serverName/index.php/module/controller/action/[param/value...]
http://localhost/zerg/public/index.php/index/index/index
URL不区分大小写
官方称为:PATH_INFO
兼容模式
http://serverName/index.php?s=module/controller/action/[param/value...]
缺点
太长
URL路径暴露出了服务器文件结构
不够灵活
不能很好的支持URL语义化(最大的缺陷)
解决:router 路由
D:\PanDownload\【微信小程序】\微信小程序商城构建全栈应用\第3章 模块,路由与获取请求参数
3-1 来编写一个简单的模块(多模块与模块命名空间)
app-模块-控制器-Index.php-操作
首大 class大写规范
单模块 清晰思路 可多 小
namespace 命名快捷键 search规范
set directory Application 编辑app 自带就不用
http://localhost/Zerg/public/index.php/koo/index/hello
入口加.php 更改要完全 默认到index
Index.php=class Index 类大写 目录小写
namespace
application/koo/controller
app/koo/controller
配置实在了Application=app
config.php
//应用命名空间
'app_namespace'=>'app'
git default open debug old
phpStorm vim i 模式用
3-2 配置虚拟域名简化URL路径
虚拟主机
z.cn->localhost
winhost-localhost-
http://localhost/Zerg/public/index.php /koo/index/hello
winhost apache简化 路由简化
+-www.z.cn 配套
虚拟主机apache识别
G:\phpStudy\Apache\conf\extra\httpd-vhosts.conf
<VirtualHost *:80>
DocumentRoot "G:\phpStudy\WWW\Zerg\public"
ServerName z.cn
一样才行了 z.cn
winHost 127.0.0.1 z.cn
win识别
c:\windows\system32\drivers\etc
问题主 apache 可以不用过
<VirtualHost *:80>
DocumentRoot "D:\server\xampp\htdocs"
ServerName localhost
localhost phpstudy-htdocs
http://z.cn/
127.0.0.1 z.cn 二级域名
127.0.0.1 www.z.cn 一级
---OK简单
phpstudy-站点域名设置 +winhost 就行了
没有配套那么麻烦
--没apache-phpinfo页面 效果达到就行了
http://z.cn/koo/index/hello
+3w
最简单也是最实用
info.php 调试 部署删除 安全隐患
3-3 安装PostMan
浏览器get请求
psotman post各个
postman other func action search
postman get post url
保存收藏
登录
3-4 三种URL访问模式
tp5路由 灵活 语义化
配置动态路由原来路径失败
route.php
use think\Route;
Route::rule('hello','koo/index/hello');
方法操作 完路径模块->
URL路径格式
PATH_INFO 完整url
混合模式
PATH_INFO router并存
强制使用路由模式
config.php
//是否开启路由
"url_route_on"=>true
默认开启混合模式 false只能path-info
//是否强制使用路由
"url_route_must"=>false
默认不强制
标准:强制开启api
团队客户端 支持一种 客户端调用 不好
{
// +----------------------------------------------------------------------
use think\Route;
//use think\facade\Url; tp5.1
use think\Url;
Route::rule('hello/:name','index/hello');//动态路由
Route::rule('demo/:name',function($name){
return $name;//http://127.0.0.1/thinkphp/public/demo/hello doc hello
});
// 添加hello路由标识
Route::rule(['kt','kt/:name'], function($name){
return 'Hello,'.$name;
});
// 根据路由标识快速生成URL
Url::build('kt', 'name=thinkphp');
// 或者使用
Url::build('kt', ['name' => 'thinkphp']);
url('kt', 'name=thinkphp');
return [
// 全局变量规则定义
'__pattern__' => [
'name' => '\w+',
],
'koo/[:name]$'=>'index/koo',//固定路由 $完整匹配
'[hello]' => [
':id' => ['index/hello', ['method' => 'get'], ['id' => '\d+']],
':name' => ['index/hello', ['method' => 'post']],
],
];
}
ThinkPHP V5.0版本是一个重构的高性能版本 基于PHP5.4
tp5 php apache mysql版本newold
-<2014201620182019->
https://jingyan.baidu.com/article/cbf0e500a6e4252eaa2893da.html
切换版本 path环境注意 替换覆盖 先上
5.4.45
5.6
7.
{
7php.init
[XDebug]
xdebug.profiler_output_dir="G:\phpStudy\tmp\xdebug"
xdebug.trace_output_dir="G:\phpStudy\tmp\xdebug"
;;zend_extension="G:\phpStudy\php\php-7.0.12-nts\ext\php_xdebug.dll"
}
5.5.38 才行了
{
Route::rule('hello','koo/Index/hello');//动态路由
删除下面
return 才行 其他版本删除也不行
}
demo 到时调式
其他版本也是->
//固定路由 $完整匹配 加[]可以+-value 不加一点传值
3-5 定义路由
完整路由表达式
Route::rule('路表达式',"路由地址","请求类型",'路由参数(数组)','变量规则(数组)');
前面3经常 后2特殊才
请求类型默认*:
GET,POST,DELETE,PUT,* 限制好 安全
Route::rule('hello','sample/Test/hello',"GET",['https'=>false]);
Route::rule('hello','sample/Test/hello',"GET|POST",['https'=>false]);
简化:
Route::get('hello','sample/Test/hello');
Route::post('hello','sample/Test/hello');
Route::any(); *请求类型
{
类型 描述
GET GET请求
POST POST请求
PUT PUT请求
DELETE DELETE请求
* 任何请求类型
}
https://www.kancloud.cn/manual/thinkphp5/
路由参数
{
参数 说明
method 请求类型检测,支持多个请求类型
ext URL后缀检测,支持匹配多个后缀
deny_ext URL禁止后缀检测,支持匹配多个后缀
https 检测是否https请求
domain 域名检测
before_behavior 前置行为(检测)
after_behavior 后置行为(执行)
callback 自定义检测方法
merge_extra_vars 合并额外参数
bind_model 绑定模型(V5.0.1+)
cache 请求缓存(V5.0.1+)
param_depr 路由参数分隔符(V5.0.2+)
ajax Ajax检测(V5.0.2+)
pjax Pjax检测(V5.0.2+)
}
3-6 获取请求参数
方法一:
Route::rule('hello/[:id]','koo/Index/hello','GET|POST',['https'=>false]);
//z.cn/hello/888?name=hello 路由定义了[:id] 888就不用id=888 id/888
public function hello($id,$name)
z.cn/hello/888/name/hello
postget都行了
postman
body 打
z.cn/hello/888/name
z.cn/hello/888/name?name=kkk name覆盖
z.cn/hello/888
z.cn/hello/888?name
?能自动
route
:id不需要加?
方法($name)
加?访问
方法二:
use think\Request;
方法内 实例
$id=Request::instance()->param('id');
request();
不区分getpost...
组手request()->param('id') 'all'
input('param.');
use think\Request;
静态方法获取实例
$request=Request::instance();
$id=$request->param('id');
$name=$request->param('name');
免实例 方法变量注入
//public function hello(Request $request)
// $id=$request->param('id');
// $name=$request->param('name');
默认param 全部所有变量
$all=Request::instance()->param();
var_dump($all);
全部数组
Postman查看 默认也行了
Preview清楚
$all=Request::instance()->get();
get只获取?号 url 以后的参数
$all=Request::instance()->route();//路径id :id
post
关Vim设Eclipse
$all=input('param.');//all
$all=input('param.id');//id
$all=input('get.id');//id get|post
dump($all);
{
变量类型方法包括:
方法 描述
param 获取当前请求的变量
get 获取 $_GET 变量
post 获取 $_POST 变量
put 获取 PUT 变量
delete 获取 DELETE 变量
session 获取 $_SESSION 变量
cookie 获取 $_COOKIE 变量
request 获取 $_REQUEST 变量
server 获取 $_SERVER 变量
env 获取 $_ENV 变量
route 获取 路由(包括PATHINFO) 变量
file 获取 $_FILES 变量
}
可以使用has方法来检测一个变量参数是否设置,如下:
Request::instance()->has('id','get');
input('?get.id');
变量修饰符
input('变量类型.变量名/修饰符');
或者
Request::instance()->变量类型('变量名/修饰符');
{
input('post.ids/a');
Request::instance()->get('id/d');
修饰符 作用
s 强制转换为字符串类型
d 强制转换为整型类型
b 强制转换为布尔类型
a 强制转换为数组类型
f 强制转换为浮点类型
}
更改变量
Request::instance()->get(['id'=>10]);
// 是否为 GET 请求
if (Request::instance()->isGet()) echo "当前为 GET 请求";
// 是否为 GET 请求
if (request()->isGet()) echo "当前为 GET 请求";
属性注入
// 动态绑定属性
Request::instance()->bind('user',new User);
// 或者使用
Request::instance()->user = new User;
request()->user;
3-7 产品功能讲解与分析
小程序常用api
demo原码 思维 webStocked cmstp二次原码 二次开发
https://blog.csdn.net/u011242029/article/details/83039589
https://github.com/Paladinhanxiao/Tp5WechatShop
websocket
https://www.runoob.com/html/html5-websocket.html
crmeb
https://gitee.com/ZhongBangKeJi/CRMEB
http://down.chinaz.com/soft/39155.htm
演示站后台
账号:demo
密码:crmeb.com
http://www.pianshen.com/article/185843969/
饮食 零食商贩
轮播图不前端写死 数据库决定
精选主题
前端固定
下拉刷新 小程序生命周期
不无限极 出问题
不经常加载数据 本地缓存
购物车 websocket
xmaied
自己三级列表联动
微信自己的管理
支付 二维码 直接微信支付
3-8 Navicat安装以及数据库设计初步分析
navicat 用户图片 修改密码
导入数据库最后一个utf-8 第一个集
数据库 运行sql文件
就是导入数据库
命令行也行
数据表设计
外面俩个是轮播图
分类表
图片信息
顶单
订单下的很多商品
商品
商品图片
商品属性
专题sing
app架构三方插件
用户信息
用户信息
外界关系 隐式 约束search
经常更新不约束 更新少才
现在别人假删除数据库 改id之类
设计数据库 一边写一边改 经验
迭代
软件设计表 下面注释 `` --
D:\PanDownload\【微信小程序】\微信小程序商城构建全栈应用\第4章 构建验证层\第4章 构建验证层
4-1 Banner数据表设计分析
api版本号
CREATE TABLE `banner` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(50) DEFAULT NULL COMMENT 'Banner名称,通常作为标识',
`description` varchar(255) DEFAULT NULL COMMENT 'Banner描述',
`delete_time` int(11) DEFAULT NULL,
`update_time` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='banner管理表';
CREATE TABLE `banner_item` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`img_id` int(11) NOT NULL COMMENT '外键,关联image表',
`key_word` varchar(100) NOT NULL COMMENT '执行关键字,根据不同的type含义不同',
`type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '跳转类型,可能导向商品,可能导向专题,可能导向其他。0,无导向;1:导向商品;2:导向专题',
`delete_time` int(11) DEFAULT NULL,
`banner_id` int(11) NOT NULL COMMENT '外键,关联banner表',
`update_time` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COMMENT='banner子项表';
id对应
一个banner可以有多个banner_item
banner_item只能属于一个banner
4-2 Banner接口定义及自定义控制器多级目录
G:\phpStudy\WWW\Zerg\application\api\controller\v1\Banner.php
{
?php
namespace app\api\controller\v1;
class Banner
{
/**
* 获得指定id的banner信息
* @url /banner/:id
* @http GET
* @id banner的id号
* 方便以后增加多个banner
*/
public function getBanner($id){
}
}
}
Route.php
{
//error
//Route::get('banner/:id','api/controller/v1/Banner/getBanner');
//模块/控制器/操作 三段式 error 多个v1 error
//Route::get('banner/:id','api/Banner/getBanner');
//模块/控制器/操作 三段式 error 多个v1 不是v1/Banner v1.Banner pt5设计的
//不是命名空间 是三段式 固定记得 不是控制器路径 但是是类名 还是大写号Banner 小写也行banner
//http://127.0.0.1/zerg/public/banner/1
//http://z.cn/banner/1
Route::get('banner/:id','api/v1.Banner/getBanner');
}
4-3 Validate:独立验证
传递过来的id合法 校验
独立验证
验证器 tp推荐
俩错+ 数组
tp5自带 设计好的
debug url后面的大写部分?---> 快捷键调式
断点}
刷新其他不动 鼠标放到 $result=$validate->batch()->check($data);
$result上变量 true 没错
半天的控制器不存在
少了 < ?php
apache php 配置和版本 问题
https://www.cnblogs.com/llkbk/p/10007217.html
4.3Validate独立校验
如何使用TP5的 validate 类 来构建我们的参数校验层.
在TP5中 validate有2种用法.
1.独立验证
2.验证器
要想使用validate首先就要引入它. use think\Validate;
快速debug
将XDEBUG_SESSION_START=16721添加到rul路径后面,不过XDEBUG_SESSION_START=16721前面要增加 ?
独立验证
{
// 编写验证规则
$validate = new Validate([
'name' => 'require|max:10',
'email' => 'email'
]);
// 执行验证 下面是单个验证
// $result = $validate->check($data);
// 不过我们需要批量验证 先调用batch方法 再调用check方法
$result = $validate->batch()->check($data);
// 打印验证出错原因
// 返回的是一个报错数组 因此就不能用 echo 而需要用var_dump()
// echo $validate->getError();
var_dump($validate->getError());
// 以上就是独立验证大方法 独立验证还分为单个验证和批量验证 批量验证需要在check之前调用batch方法 输出的时候也不能用echo
}
TP5的validate提供了很多写好的验证方法
比如 require max:10 email .... 更多可见文档
https://www.kancloud.cn/manual/thinkphp5/129356
当内置的验证规则不能使用的时候我们就需要自定义验证规则
验证器的验证方式是官方推荐的验证方法
{
< ?php
namespace app\api\controller\v1;
use think\validate;//导入校验
class Banner
{
/**
* 获得指定id的banner信息
* @url /banner/:id
* @http GET
* @id banner的id号
* 方便以后增加多个banner
*/
public function getBanner($id)
{
$data=[
'name'=>"koo",
'email'=>'koohello@qq.com'
];
// 编写验证规则
$validate=new Validate([
'name'=>'require|max:5',//tp5内置验证的规则
'email'=>'email'
]);
// 执行验证 下面是单个验证
// $validate->check($data);
// 不过我们需要批量验证 先调用batch方法 再调用check方法
$result=$validate->batch()->check($data);
// 打印验证出错原因
// 返回的是一个报错数组 因此就不能用 echo 而需要用var_dump()
// echo $validate->getError();
var_dump($validate->getError());
// 以上就是独立验证大方法 独立验证还分为单个验证和批量验证 批量验证需要在check之前调用batch方法 输出的时候也不能用echo
//独立验证
//验证器
}
}
}
内置规则
'name'=>'require'
'num'=>'number'
'num'=>'float'
'num'=>'float'
'email'=>'email'
'info'=>'array'
验证某个字段是否为为 yes, on, 或是 1。这在确认"服务条款"是否同意时很有用,例如:
'accept'=>'accepted'
'date'=>'date'
验证某个字段的值是否为字母,例如:
'name'=>'alpha'
验证某个字段的值是否为字母和数字,例如:
'name'=>'alphaNum'
验证某个字段的值只能是汉字,例如:
'name'=>'chs'
'ip'=>'ip'
正则验证
'zip'=>'\d{6}',
'score'=>'elt:100'
'num'=>'<=:100'
使用助手函数实例化验证器
$validate = validate('User');
rule方法动态添加规则
$validate->rule('zip', '/^\d{6}$/');
自定义验证规则
{
namespace app\index\validate;
use think\Validate;
class User extends Validate
{
protected $rule = [
'name' => 'checkName:thinkphp',
'email' => 'email',
];
protected $message = [
'name' => '用户名必须',
'email' => '邮箱格式错误',
];
// 自定义验证规则
protected function checkName($value,$rule,$data)
{
return $rule == $value ? true : '名称错误';
}
}
}
静态调用
// 日期格式验证
Validate::dateFormat('2016-03-09','Y-m-d'); // true
// 验证是否有效的日期
Validate::is('2016-06-03','date'); // true
// 验证是否有效邮箱地址
Validate::is('thinkphp@qq.com','email'); // true
// 验证是否在某个范围
Validate::in('a',['a','b','c']); // true
// 验证是否大于某个值
Validate::gt(10,8); // true
// 正则验证
Validate::regex(100,'\d+'); // true
4-4 Validate验证器
//保护 $rule固定
use think\Validate
class Xxx extends Validate{
protected $rule=[
'name'=>'require|max:5',
'email'=>'email'
];
}
其他php
$validate =new Xxx();
验证器相对于独立验证的优点是做了更好的封装
4.4.1 首先在api目录下新建一个validate文件夹.
4.4.2 然后在validate文件夹新建一个名为TestValidate.php 类.
4.4.3 class TestValidate 需要继承 Validate
{
use think\Validate;
class TestValidate extends Validate
{
// $rule 是固定的数组 名字不能改
protected $rule = [
'name' => 'require|max:10',
'email' => 'email'
];
// 以上就定义完毕了
// 然后就可以去Banner去使用这个验证器
}
}
Banner.php
{
$data = [
'name' => 'sdasdsazhangbin',
'email' => '739367755qq.com'
];
// 这样是用了验证器 而不是独立验证 这里要主要 new的是刚才编写的class 而不是Validate
$validate = new TestValidate();
// 执行验证 下面是单个验证
// $result = $validate->check($data);
// 不过我们需要批量验证 先调用batch方法 再调用check方法
$result = $validate->batch()->check($data);
// 打印验证出错原因
// 返回的是一个报错数组 因此就不能用 echo 而需要用var_dump()
// echo $validate->getError();
var_dump($validate->getError());
}
表面看 独立验证 和 验证器 没有什么大的区别,底层原理就是一样的,
但是在大的项目复杂的逻辑下才能体会到验证器的好处.
4-5 零食商贩结构体系详解
在线思维导图
processon.com
图片see
Model一般放的是粒度比较细的业务
而Service是对Model层粒度的组装
但是Model和Service统称为业务层,
就是业务复杂的时候要用到Service
不复杂的时候直接走Model层 因为Model和Service是平行关系.
业务层是通过Think DB 框架去调用Mysql 从而获得相关的业务数据
这个图适合中小型项目-->
4.5over 但是我觉得挺重要的.
4-6 自定义验证规则
直白 多id长
通常
$id 不能是负数等
tp5是否有规则 没有自己div
其他也要调用 封装函数
重复
直接extends 编辑器自动添加use
f2调到源代码 设置过eclipse变成ctrt+左点击
IDMustBePostiveInt.php
{
< ?php
namespace app\api\validate;
use think\Validate;
class IDMustBePostiveInt extends Validate
{
protected $rule=[
'id'=>'require|isPostiveInt' //tp5没有调用自己
];
//$value传来的id的值 $field=id $rule空不讲到时回头学 $data=array[id=xx]
protected function isPostiveInt($value,$rule='',$data='',$field=''){
//先判断是不是数字 是不是整数 大于0的数(不是0或者负数)
bug if (is_numeric($value)&&is_int($value+0)&&($value+0)>0){
return true;
}
else{
return $field.'必须是正整数';
}
}
}
}
Banner.php
{
< ?php
namespace app\api\controller\v1;
use app\api\validate\IDMustBePostiveInt;
use app\api\validate\TestValidate;
use think\validate;//导入校验
class Banner
{
/**
* 获得指定id的banner信息
* @url /banner/:id
* @http GET
* @id banner的id号
* 方便以后增加多个banner
*/
public function getBanner($id)
{
//if($id)
$data=[
'id'=>$id
];
//// 编写验证规则
// $validate=new Validate([
// 'id'=>'',
// ]);
$validate=new IDMustBePostiveInt();
bug $result=$validate->batch()->check($data);
//判断result true false 后干什么 业务逻辑
bug if($result){
}
else{
}
}
}
}
//这里写的isPositiveInteger其实就是扩展Validate的内置规则
4-7 工欲善其事必先利器:构建接口参数校验层
//if($id)
input('get.name')
input('get.age')
...
多参数还是很长
代码分离 封装
validate
多了
提取出来做单独类库
第三方各类库 也是这样思想
组合调用验证器
都调用的
定义一个基类
banner
{
< ?php
namespace app\api\controller\v1;
use app\api\validate\IDMustBePostiveInt;
use app\api\validate\TestValidate;
use think\validate;//导入校验
class Banner
{
/**
* 获得指定id的banner信息
* @url /banner/:id
* @http GET
* @id banner的id号
* 方便以后增加多个banner
*/
public function getBanner($id)
{
//简化 拦截器 idm又getCheck 可能 base idm都继承Validate eror
//pass idm extends base
(new IDMustBePostiveInt())->goCheck();
$a=3;
// $data=[
// 'id'=>$id
// ];
//
////// 编写验证规则
//// $validate=new Validate([
//// 'id'=>'',
//// ]);
// $validate=new IDMustBePostiveInt();
// $result=$validate->batch()->check($data);
// //判断result true false 后干什么 业务逻辑
// if($result){
//
// }
// else{
//
// }
}
}
}
IDMustBePostiveInt
{
< ?php
namespace app\api\validate;
use think\Validate;
class IDMustBePostiveInt extends BaseValidate
{ //同级继承不加use 也不自动
protected $rule=[
'id'=>'require|isPostiveInt' //tp5没有调用自己
];
//$value传来的id的值 $field=id $rule空不讲到时回头学 $data=array[id=xx]
//这里写的isPositiveInteger其实就是扩展Validate的内置规则
protected function isPostiveInt($value,$rule='',$data='',$field=''){
//先判断是不是数字 是不是整数 大于0的数(不是0或者负数)
if (is_numeric($value)&&is_int($value+0)&&($value+0)>0){
return true;
}
else{
return $field.'必须是正整数';
}
}
}
}
BaseValidate
{
< ?php
namespace app\api\validate;
use think\Exception;
use think\Request;
use think\Validate;
class BaseValidate extends Validate
{
public function goCheck(){
//获取http传入的参数
//对这些参数做校验
$request=Request::instance();//获得请求实例
$params=$request->param();//获得所有的参数
$result=$this->check($params);//继承Validate 在内部不用new
if (!$result){
//失败时干什么 把错误信息返回 用户那错误
//getEttor()获取错误信息 这是调用设置 不用get
$error=$this->error;
//抛出异常 把访问中断
//其实到时自定义异常 错误信息不容易看 信息安全...
throw new Exception($error);//tp5默认异常
}
else{
return true;//成功返回true
}
}
}
}
5-1 介绍下REST之前的重要协议SOAP
什么是rest
Representational State Transfer:
表述性状态转移
一种风格,约束,设计理念
用描述json数据
https://baike.baidu.com/item/rest/6330506?fr=aladdin
SOAP vs Rest
简单对象访问属性原型
@SOAP Simple Object Access Protocol
重点
通常来说,使用xml描述数据
https://www.runoob.com/soap/soap-tutorial.html
https://baike.baidu.com/item/%E7%AE%80%E5%8D%95%E5%AF%B9%E8%B1%A1%E8%AE%BF%E9%97%AE%E5%8D%8F%E8%AE%AE/3841505?fromtitle=SOAP&fromid=4684413&fr=aladdin
json|xml 通用 语言 数据交互
网页js-后台-json|xml soap 以前
rest 优先
介绍REST
什么是REST?
中文意思 表述性状态转移, 是一种风格,约束或者说设计理念
SOAP vs REST
在REST出现之前,一直就是SOAP的天下
@SOAP Simple Object Access Protocol 通常来说,使用XML来描述数据,比较重,相对于轻量级来说.REST提倡用json来描述数据.
REST给我们提供了更加轻量级的思维方式.
就目前的形式来看,大多数互联网的产品在提供接口的时候还是会优先选择REST,但是并不意味着SOAP就没有人去用,在传统的企业级时候还是占一席之地.
5-2 REFTFul API的特点解析
什么是REST
@RESTFul API 基于REST的api设计理论
轻
通常来说,使用json描述数据
无状态
好像同异步差不多
@RESTFul Api
基于资源,增删改查都只是对于资源状态的改变
使用HTTP动词来操作资源
对URL 不要出现名词 动词
动词
get pull delete select
soap传统get获取 还删除 可以
rest 不可用 意义不明确
@RESTFul API
/getmovie/:mid
GET:/movie/:mid 建议
RESTFul API 基于REST的API设计理论
特点
1.轻
2.通常来说,使用JSON描述数据
3.无状态 (也是REST一个很大的特点),可以这样理解,发送了2个http请求,它们之间没有任何关系,第二个也不会依赖第一个http请求.
@RESTFul API 基于资源,增删改查都只是对于资源状态的改变
使用http动词来操作资源.
使用url来表示资源.
在传统开发中和在REST中 get,post的区别
从技术层次来讲没有区别,但是从意义上来讲是有区别的,在传统开发中我们选择get,post的选择依据并不是根据资源的增删改查,比如你又可能使用get方法去删除一个数据,这在传统的web开发中是没有区别的,但是在REST开发中是绝对不行的,因为在REST里面get表示的是一个查询的操作,因此你在REST中用get删除一个数据语义是不明确的,也是非常不合理的.
总结:在传统的开发中,不管你是增伤改查,只要你的参数足够简单就可以使用get方法.而你的参数比较复杂,比如你要提交一个表单,在这种情况下我们就会使用post
但是对于REST服务来说,选择get或者post的依据是到底你是查询操作还是新增操作.
@RESTFul API
/getmovie/:mid 不推荐
GET: /movie/:mid 推荐
5-3 RESTFul API的最佳实践_x264
RESTFUL特点包括:
1、每一个URI代表1种资源;
2、客户端使用GET、POST、PUT、DELETE4个表示操作方式的动词对服务端资源进行操作:GET用来获取资源,POST用来新建资源(也可以用于更新资源),PUT用来更新资源,DELETE用来删除资源;
3、通过操作资源的表现形式来操作资源;
4、资源的表现形式是XML或者HTML;
5、客户端与服务端之间的交互在请求之间是无状态的,从客户端到服务端的每个请求都必须包含理解请求所必需的信息。
https://baike.baidu.com/item/RESTful/4406165?fr=aladdin
RESTFul API最佳实践
@HTTP动词(幂等性,资源安全性)
POST:创建
PUT:更新
GET:查询
DELETE:删除
状态码:404,400 200 201 202 401 403 500
错误码:自定义的错误id号
统一描述错误:错误码,错误信息,当前URL
get200 push201 根据框架不同
404 网页找不到 资源找不到REST
400 参数错误
200 get 查询操作
201 push 操作资源创建成功
202 pul资源创建成功
网页请求已发送,状态还没
一些api 更新为成功 不标准
401 未授权
403 当前资源被禁止 有授权
a操作b 403
500 服务器未知错误
代码bug不知道
不让客户端知道 明白
...405 407
RESTFul API
使用Token令牌来授权和验证身份
版本控制
测试与生产环境分开
api.xxx.com
dev.api.xxx.com
网站用cookies|session保存
tp用Token令牌来
URL语义要明确,最好可以"望文知意"
最好是有一份比较标准的文档
@HTTP动词 (幂等性 资源安全性, 一般用不到除非安全性非常高)
状态码:
404 当前请求的页面没有找到
400 参数错误
200 一个查询操作 get请求成功
201 post创建资源成功
202 put更新成功
401 未授权
403 当前这个资源是被禁止的
500 服务器的未知错误
错误码:
自定义的错误ID号 就像开发微信时给你个报错码 你去微信官网去查找对应错误码的信息
统一描述错误: 错误码 错误信息 当前URL
使用Token令牌来授权和验证身份
Cookie和Token在本质上没有太大的区别,但是在实现的机制上有一些区别,因为Cookie一般是浏览器上的行为,每一次访问浏览器会自动携带cookie,而Token通常我们自己存储和管理的.所以我认为Token会更加灵活一点.
Token是一个重点,也会详细讲明它的应用.
5-4 如何学习RESTFul API设计
RESTFul api最佳实践
学习RESTFul api的最佳方式
模仿
豆瓣开放api 简单
search 官网
Github开发者api 完整
https://developer.github.com/v3
有空到时
RESTFul API的合理使用(切勿盲目照搬标准REST)
内部查 不开放
俩个不同查询的接口分开俩个类来
混乱
a查a b->b
模仿(豆瓣开发api 非常标准的:GitHub开发者API)
ctrl + alt + o 快速删除无用的命名空间
https://developer.github.com/v3/
https://www.jianshu.com/p/a0c7d0482415
https://www.cnblogs.com/e0yu/p/8527114.html
https://developers.douban.com/wiki/?title=api_v2
https://www.jianshu.com/p/5ada97c547b7
豆瓣关了 视频没截图 但关了测试不了 过 次要盘有 有再下
还没真学过 ->
https://wenku.baidu.com/view/f1ee47df6f1aff00bfd51e05.html
https://github.com/zce/douban-api-docs
https://douban-api-docs.zce.me/
https://github.com/zce/douban-api-proxy
https://douban.uieee.com/
D:\PanDownload\【微信小程序】\微信小程序商城构建全栈应用\第6章 AOP与全局异常处理\第6章 AOP与全局异常处理
6-1 正确理解异常处理流程
业务处理业务层model
伪代码
命名 不太好 长 情况决定
BannerController.php
BannerModel.php
文件路径无意义
删除无语命名空间
ctrl+alt+O
不能用 \app\\调用 在方法 不美观
use \\\ as 别名
正常返回
有可能出错
数据库不存在 其他...
异常基础框架
trycatch
全局异常处理(AOP)
方法3Exception
方法2Exception
方法1Exception
{
异常捕获
处理(记录日志或者修复异常或者返回客户端错误)
抛出异常 一层层抛
}
纠正过返回客户端 好 一般不
一层层抛 复杂
全局异常处理(AOP)
记录日志 未知错误
RESTFul 错误码等在这写
不处理返回客户端500 未知错误
能告诉客户端是什么就什么 都好不500
TP提供全局异常处理类
重写
{
< ?php
namespace app\api\controller\v1;
use app\api\validate\IDMustBePostiveInt;
use app\api\model\Banner as BannerModel;// as 别名
class Banner
{
/**
* 获得指定id的banner信息
* @url /banner/:id
* @http GET
* @id banner的id号
* 方便以后增加多个banner
*/
public function getBanner($id)
{
(new IDMustBePostiveInt())->goCheck();
$banner=BannerModel::getBannerByID($id);
return $banner;
}
}
}
{
< ?php
namespace app\api\model;
class Banner
{
public static function getBannerByID($id){
//TODO:根据Banner ID号 获取Banner信息 伪信息
return 'this is banner info';
}
}
}
ctrl + alt + o 快速删除无用的命名空间
6-2 固有的处理异常的思维模式与流程
抛到控制器
不做任何处理 error 不好看 客户端无法处理
config
debug=开关
返回还是tp自带错误信息
http网页可以
但是api开发不行 客户端也是网页信息客户端无法处理
开发api 和开发网页是不同的
还抛 抛到tp5自带的全局异常处理
return 直接抛会错误 数组
转换为json 加状态码400 客户端才好处理
不改状态码 默认200 表示成功
不好 本身错误
改错误400
postman 工具内容 右上边有状态码看看
整体 的代码错误 容易懂
以后 正确 优化代码
其他的api设计
对比
/0错误自带的错误 没必要返回客户端
一些返 一些不
记录下来 告诉这里错误就行
没对比就不知道好处
绕来绕去
抽象
封装性好
适应代码变化强
{
< ?php
namespace app\api\model;
use think\Exception;
class Banner
{
public static function getBannerByID($id){
//TODO:根据Banner ID号 获取Banner信息 伪信息
try{
1/0;//如果是变量 复杂 返回错误网页 客户端是json处理 则崩溃
}catch (Exception $ex){
//TODO:可以记录日志
throw $ex;//往上抛 banner控制器
}
return 'this is banner info';
}
}
}
{
< ?php
namespace app\api\controller\v1;
use app\api\validate\IDMustBePostiveInt;
use app\api\model\Banner as BannerModel;
use think\Exception;// as 别名
class Banner
{
/**
* 获得指定id的banner信息
* @url /banner/:id
* @http GET
* @id banner的id号
* 方便以后增加多个banner
*/
public function getBanner($id)
{
(new IDMustBePostiveInt())->goCheck();
//太直白不好 其他错误异常 其他接口 再写复杂多 理解异常处理流程
try{
$banner=BannerModel::getBannerByID($id);
}catch(Exception $ex){
$err=[
'error_code'=>10001,//状态码怎么用
'msg'=>$ex->getMessage() //RESTFul
];
return json($err,400);//转json 状态码 直接是数组错误
}
return $banner;
}
}
}
开启关闭debug模式: 在config.php文件下 'app_debug' => false,
常规的异常处理 不够灵活切复用性比较低
// 这里捕获的异常是调用getBannerByID方法时候 getBannerByID方法内部抛出的异常
// 调用BannerModel下的静态方法getBannerByID()
// 利用TP5的json函数将数组转化为json
// 400表示的是返回的状态码
代码越抽象 代码的复用性就越高
6-3 理清思路,总结异常的分类
异常分类
通常近况
特殊抓ip那些
由于用户行为导致的异常(没有通过验证器,没查询到结果)
通常不需要记录日志 记录大 特殊记录
需要向用户返回具体信息
一些api没结果算是异常 一些直接空数组
看情况
不一定 观察一段时间
想出来 多写代码 总结
不好 回头 重构代码
服务器自身异常(代码错误/0,调用外部接口错误wx等)
通常记录日志
不向客户端返回具体原因
客户端也不懂不处理
异常的分类:
1.由于用户行为导致的异常(比如没有通过验证器,没查询到结果)--通常不需要记录日志,需要向用户返回具体信息.
2.服务器自身的异常(代码错误,调用外部接口错误)--通常需要记录日志,不向用户通知具体信息.
抛出异常,如果没有在特定的地方捕获异常,那么这个异常就会抛到全局异常处理哪里.最后交给Handle类下的render方法去处理异常.
我们可以重写render方法,不过在重写的同时需要修改config.php文件
6-4 实现自定义全局异常处理 上
api同级 里面也可以
exception 全局处理 不属于里面 通用 很多个模块来调用
自己理解位置而定
lib 类库 其他项目也可以用
良好封装基础类库 不public
统一描述信息
预变量
语语单词变量好
error.code 参考
{
999 未知错误
1 开头为通用错误
2 商品类错误
3 主题类错误
4 Banner类错误
5 类目类错误
6 用户类错误
8 订单类错误
10000 通用参数错误
10001 资源未找到
10002 未授权(令牌不合法)
10003 尝试非法操作(自己的令牌操作其他人数据)
10004 授权失败(第三方应用账号登陆失败)
10005 授权失败(服务器缓存异常)
20000 请求商品不存在
30000 请求主题不存在
40000 Banner不存在
50000 类目不存在
60000 用户不存在
60001 用户地址不存在
80000 订单不存在
80001 订单中的商品不存在,可能已被删除
80002 订单还未支付,却尝试发货
80003 订单已支付过
}
config.php
{
// 异常处理handle类 留空使用 \think\exception\Handle
'exception_handle' => 'app\lib\exception\ExceptionHandler',
}
v1\Banner
{
< ?php
namespace app\api\controller\v1;
use app\api\validate\IDMustBePostiveInt;
use app\api\model\Banner as BannerModel;
use think\Exception;// as 别名
class Banner
{
/**
* 获得指定id的banner信息
* @url /banner/:id
* @http GET
* @id banner的id号
* 方便以后增加多个banner
*/
public function getBanner($id)
{
(new IDMustBePostiveInt())->goCheck();
$banner=BannerModel::getBannerByID($id);
//不做异常处理往上抛
return $banner;
}
}
}
app\lib\exception
{
< ?php
namespace app\lib\exception;
use Exception;
use think\exception\Handle;
class ExceptionHandler extends Handle
{
public function render(Exception $e)
{
//要想生效开config exception 默认空默认tp5自带 exception_handle=''
return json('~~~~~~~~');
}
}
}
{
< ? php
namespace app\lib\exception;
class BaseException
{
//HTTP状态码 404,200 通用的
public $code=400;
//错误具体信息
public $msg='参数错误';
//自定义的错误码
public $errorCode=10000;
}
}
{
< ?php
namespace app\lib\exception;
class BannerMissException extends BaseException
{
public $code=404;
public $msg='请求的Banner不存在';
public $errorCode=40000;
}
}
抛出异常,如果没有在特定的地方捕获异常,那么这个异常就会抛到全局异常处理哪里.最后交给Handle类下的render方法去处理异常.
我们可以重写render方法,不过在重写的同时需要修改config.php文件
6-5 实现自定义全局异常处理 下
ExceptionHandler
{
< ?php
namespace app\lib\exception;
use Exception;
use think\exception\Handle;
use think\Request;
class ExceptionHandler extends Handle
{
private $code;
private $msg;
private $errorCode;
//需要返回客户端当前请求的url路径
public function render(Exception $e)
{
// //要想生效开config exception 默认空默认tp5自带 exception_handle=''
// return json('~~~~~~~~');
if($e instanceof BaseException){
//如果是自定义的异常 用户异常返回信息 不记录 BaseException
$this->code=$e->code;
$this->msg=$e->msg;
$this->errorCode=$e->errorCode;
}else{
$this->code=500;
$this->msg="服务器内部错误,不想告诉你";
$this->errorCode=999;
}
$request=Request::instance();
$result=[
'msg'=>$this->msg,
'error_code'=>$this->errorCode,
// 不是在这用 'code'=>$this->code,
'request_url'=>$request->url()
];
//这
return json($result,$this->code);
}
}
}
BaseException
{
< ?php
namespace app\lib\exception;
use think\Exception;
class BaseException extends Exception
{
//HTTP状态码 404,200 通用的
public $code=400;
//错误具体信息
public $msg='参数错误';
//自定义的错误码
public $errorCode=10000;
}
}
model\Banner
{
< ?php
namespace app\api\model;
use think\Exception;
class Banner
{
public static function getBannerByID($id){
//TODO:根据Banner ID号 获取Banner信息 伪信息
// try{
// 1/0;//如果是变量 复杂 返回错误网页 客户端是json处理 则崩溃
// }catch (Exception $ex){
// //TODO:可以记录日志
// throw $ex;//往上抛 banner控制器
// }
// return 'this is banner info';
return null;
}
}
}
v1\Banner
{
< ?php
namespace app\api\controller\v1;
use app\api\validate\IDMustBePostiveInt;
use app\api\model\Banner as BannerModel;
use app\lib\exception\BannerMissException;
use think\Exception;// as 别名
class Banner
{
/**
* 获得指定id的banner信息
* @url /banner/:id
* @http GET
* @id banner的id号
* 方便以后增加多个banner
*/
public function getBanner($id)
{
(new IDMustBePostiveInt())->goCheck();
$banner=BannerModel::getBannerByID($id);
if (!$banner){
throw new BannerMissException();
}
//不做异常处理往上抛
return $banner;
}
}
}
6-6 ThinkPHP5中的日志系统
代码错误断点输出等调试
生产环境 靠日志
特征日志变量id等 只能在操作上 记录下来
全局 不能
tp5自动记录错误日志
格式不够diy
关闭
再自定义
tp5
配置文件主要都
config
database.php上
工具查
判断位置-->->
日志保存在runtime-log
config.php
'log' => [
// 日志记录方式,内置 file socket 支持扩展
'type' => 'File',
// 日志保存目录
'path' => LOG_PATH,
// 日志记录级别
'level' => [],
],
习惯可以不改日志 tp自带比较全
入口文件改
index.php
{
< ?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2016 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st
// +----------------------------------------------------------------------
// [ 应用入口文件 ]
// 定义应用目录
define('APP_PATH', __DIR__ . '/../application/');
define('LOG_PATH',__DIR__.'/../log/');
// 加载框架引导文件
require __DIR__ . '/../thinkphp/start.php';
}
自动创建文件
日子文件比较全
调重要
有选择记录日志 大
日志在生产阶段的时候会用的多一些
TP5默认会记录日志异常,但是我们很多时候需要自己去定义日志的格式.所以我们要去把TP5默认的日志关闭掉.配置主要在config.php和database.php下.
TP5默认生成的日志在runtime->log目录下
1.更改Log目录位置,在public\index.php下
index.php 改变了LOG_PATH的路径
重新一运行,如果有异常就会在zerg下生成一个新的目录log
2.为什么要自定义日志格式,因为日志也有的是保存的是用户操作不当而产生的异常,这样的异常非常多切不是服务器的问题,我们只需要记录服务器内部产生的异常,因为我们需要自定义日志格式.
因此我们开始关闭默认的日志格式,编写自己的日志格式.
6-7 在全局异常处理中加入日志记录
config.php
关闭日志 把 'type' => 'test',
{
'log' => [
// 日志记录方式,内置 file socket 支持扩展
'type' => 'File',
// 日志保存目录
'path' => LOG_PATH,
// 日志记录级别
'level' => [],
],
}
ExceptionHandler
定义一个方法
log::record($e->getMessage(),'error')
log类的 静态方法 错误信息 错误级别error
关闭:在aplication\config.php下,将默认的file改为test
因为我们刚才关闭了log因此我们再使用log的时候需要初始化
如果在开发阶段的时候,我们将config.php的app_debug设置为true 当转为生产模式的时候我们应该将它改为false
ctrl+b 跳转到声明处
定义一个变量 就是跳到 声明的地方
log
{
---------------------------------------------------------------
[ 2019-10-25T16:02:04+08:00 ] 127.0.0.1 GET z.cn/banner/1
[运行时间:0.052035s] [吞吐率:19.22req/s] [内存消耗:1,646.25kb] [文件加载:42]
[ error ] [0]内部错误[G:\phpStudy\WWW\Zerg\application\api\controller\v1\Banner.php:24]
[ error ] 内部错误
}
v1\Banner
{
< ?php
namespace app\api\controller\v1;
use app\api\validate\IDMustBePostiveInt;
use app\api\model\Banner as BannerModel;
use app\lib\exception\BannerMissException;
use think\Exception;// as 别名
class Banner
{
/**
* 获得指定id的banner信息
* @url /banner/:id
* @http GET
* @id banner的id号
* 方便以后增加多个banner
*/
public function getBanner($id)
{
(new IDMustBePostiveInt())->goCheck();
$banner=BannerModel::getBannerByID($id);
if (!$banner){
// log('error');
// throw new BannerMissException(); 自定义错误
throw new Exception('内部错误');
}
//不做异常处理往上抛
return $banner;
}
}
}
ExceptionHandler
{
< ?php
namespace app\lib\exception;
use Exception;
use think\exception\Handle;
use think\Log;
use think\Request;
class ExceptionHandler extends Handle
{
private $code;
private $msg;
private $errorCode;
//需要返回客户端当前请求的url路径
public function render(Exception $e)
{
// //要想生效开config exception 默认空默认tp5自带 exception_handle=''
// return json('~~~~~~~~');
if($e instanceof BaseException){
//如果是自定义的异常 用户异常返回信息 不记录 BaseException
$this->code=$e->code;
$this->msg=$e->msg;
$this->errorCode=$e->errorCode;
}else{
$this->code=500;
$this->msg="服务器内部错误,不想告诉你";
$this->errorCode=999;
$this->recordErrorLog($e);
}
$request=Request::instance();
$result=[
'msg'=>$this->msg,
'error_code'=>$this->errorCode,
// 不是在这用 'code'=>$this->code,
'request_url'=>$request->url()
];
//这
return json($result,$this->code);
}
private function recordErrorLog(Exception $e){
// 配置关了 要手动开启日志输出
// 我们已经在config.php哪里关闭了log 因此需要在这路初始化log
Log::init([
'type'=>'File',
'path'=>LOG_PATH,
'level'=>['error'] //错误级别 只记录错误以上的 低于不记录
]);
//错误信息引进来 静态方法 错误信息 错误级别
Log::record($e->getMessage(),'error');
}
}
}
日志写入
手动记录
方法 描述
Log::record() 记录日志信息到内存
Log::save() 把保存在内存中的日志信息(用指定的记录方式)写入
Log::write() 实时写入一条日志信息
record方法用法如下:
Log::record('测试日志信息');
默认的话记录的日志级别是INFO,也可以指定日志级别:
Log::record('测试日志信息,这是警告级别','notice');
采用record方法记录的日志信息不是实时保存的,如果需要实时记录的话,可以采用write方法,例如:
Log::write('测试日志信息,这是警告级别,并且实时写入','notice');
日志级别
ThinkPHP对系统的日志按照级别来分类,并且这个日志级别完全可以自己定义,系统内部使用的级别包括:
{
log 常规日志,用于记录日志
error 错误,一般会导致程序的终止
notice 警告,程序可以运行但是还不够完美的错误
info 信息,程序输出信息
debug 调试,用于调试信息
sql SQL语句,用于SQL记录,只在数据库的调试模式开启时有效
}
Log::error('错误信息');
Log::info('日志信息');
// 和下面的用法等效
Log::record('错误信息','error');
Log::record('日志信息','info');
助手函数
trace('错误信息','error');
trace('日志信息','info');
日志自动清理
V5.0.16+版本开始,支持文件日志的自动清理功能,你可以设置
'max_files' => 30
则日志文件最多只会保留30个,超过会自动清理较早的日志文件,避免日志文件长期写入占满磁盘空间。
日志初始化
Log::init([
'type' => 'File',
'path' => APP_PATH.'logs/'
]);
// 可以临时关闭日志写入
'type' => 'test',
File驱动
日志的记录方式默认是File方式,可以通过驱动的方式来扩展支持更多的记录方式。
Scoket驱动
远程调试部分。
// error和sql日志单独记录
'apart_level' => ['error','sql'],
日志清空
Log::clear();
写入授权
// 设置IP为授权Key
Log::key(Request::instance()->ip());
'log' => [
// 日志类型为File
'type' => 'File',
// 授权只有202.12.36.89 才能记录日志
'allow_key' => ['202.12.36.89'],
]
6-8 全局异常处理的应用 上
没有指定具体错误原因
// TODO进行注释
项目管理者要求代码编写人员在式样无法确定,或者其他原因不能完成编程的时候
Param注解括号内的参数进行引用
6-9 全局异常处理的应用 中
重构代码 --->好
// 应用调试模式
'app_debug' => true,
ParameterException
{
< ?php
namespace app\lib\exception;
class ParameterException extends BaseException
{
//参数异常错误
public $code=400;
public $msg='参数错误';//客户端还是不能处理 具体那里错误 调用时覆盖 现在通用
public $errorCode=10000;
}
}
BaseException
{
< ?php
namespace app\lib\exception;
use think\Exception;
use Throwable;
class BaseException extends Exception
{
//HTTP状态码 404,200 通用的
public $code=400;
//错误具体信息
public $msg='参数错误';
//自定义的错误码
public $errorCode=10000;
//构造方法
public function __construct($params=[])
{
if (!is_array($params)){
//如果不是数组就return 回去 用默认的
return ;
// throw new Exception('参数必须是数组');强制是数组 用那个都行 理解 使用方式
}
//数组中是否包含 array_key_exists(包含内容,数组) 包含就赋值 覆盖
if (array_key_exists('code',$params)){
$this->code=$params['code'];
}
if (array_key_exists('msg',$params)){
$this->msg=$params['msg'];
}
if (array_key_exists('errorCode',$params)){
$this->errorCode=$params['errorCode'];
}
}
}
}
BaseValidate
{
< ?php
namespace app\api\validate;
use app\lib\exception\ParameterException;
use think\Exception;
use think\Request;
use think\Validate;
class BaseValidate extends Validate
{
public function goCheck(){
//获取http传入的参数
//对这些参数做校验
$request=Request::instance();//获得请求实例
$params=$request->param();//获得所有的参数
$result=$this->check($params);//继承Validate 在内部不用new
if (!$result){
$e=new ParameterException([
'msg'=>$this->error,
// 'code'=>400,
// 'errorCode'=>10002
]);
// 俩种赋值都行了 但是上面的比较符合面向对象
// $e->msg=$this->error;//具体错误信息
// $e->errorCode=10002;
throw $e;
//失败时干什么 把错误信息返回 用户那错误
//getEttor()获取错误信息 这是调用设置 不用get
// $error=$this->error;
//抛出异常 把访问中断
//其实到时自定义异常 错误信息不容易看 信息安全...
// throw new Exception($error);//tp5默认异常
}
else{
return true;//成功返回true
}
}
}
}
6-10 全局异常处理的应用 下
代码重构 不是一口气完成
http://z.cn/banner/0.2?num=4
batch
'num'=>'in:1,2,3'
6-11 本章小结与AOP思想
IDMustBePostiveInt
{
< ?php
namespace app\api\validate;
use think\Validate;
class IDMustBePostiveInt extends BaseValidate
{ //同级继承不加use 也不自动
protected $rule=[
'id'=>'require|isPostiveInt', //tp5没有调用自己
'num'=>'in:1,2,3' //num必须在 1,2,3 范围内
];
//$value传来的id的值 $field=id $rule空不讲到时回头学 $data=array[id=xx]
//这里写的isPositiveInteger其实就是扩展Validate的内置规则
protected function isPostiveInt($value,$rule='',$data='',$field=''){
//先判断是不是数字 是不是整数 大于0的数(不是0或者负数)
if (is_numeric($value)&&is_int($value+0)&&($value+0)>0){
return true;
}
else{
return $field.'必须是正整数';
}
}
}
}
BaseException
{
< ?php
namespace app\api\validate;
use app\lib\exception\ParameterException;
use think\Exception;
use think\Request;
use think\Validate;
class BaseValidate extends Validate
{
public function goCheck(){
//获取http传入的参数
//对这些参数做校验
$request=Request::instance();//获得请求实例
$params=$request->param();//获得所有的参数
$result=$this->batch()->check($params);//继承Validate 在内部不用new
if (!$result){
$e=new ParameterException([
'msg'=>$this->error,
// 'code'=>400,
// 'errorCode'=>10002
]);
// 俩种赋值都行了 但是上面的比较符合面向对象
// $e->msg=$this->error;//具体错误信息
// $e->errorCode=10002;
throw $e;
//失败时干什么 把错误信息返回 用户那错误
//getEttor()获取错误信息 这是调用设置 不用get
// $error=$this->error;
//抛出异常 把访问中断
//其实到时自定义异常 错误信息不容易看 信息安全...
// throw new Exception($error);//tp5默认异常
}
else{
return true;//成功返回true
}
}
}
}
AOP 面向切面思想
电影院 不管哪里卖票
想进去,统一验票
代码不是一次性就能写好的,要不要的重构代码才能使代码更精炼,
有人说代码写着很简单.
那是因为没有考虑到代码的复用性还有代码的层次结构,
仅仅的还是停留在写业务的层次上
,如果不考虑多那么自己的代码不会得到提高,
因为代码你一直写的很直白.比如说你验证层,你直接写业务没有想到把这些抽象为一个层.
大家拿一个语言,一个框架吃透之后,其他的语言和框架在思路上都是大同小异的.
所以说学习,不是去学习具体的一门语言和一种框架,更重要的去学习编程的思路.
逐步培养自己的抽象的编程思维,
@AOP 面向切面编程
AOP的运用是非常广泛的,AOP是一种思想,并不是具体的框架也不是具体的代码.
是一个好的程序员和一般程序员的分水岭
AOP的理论知识很容易让人迷乱,不要死扣理论
通过写的实例,自己写的validate和exception就是很好的AOP编程
尤其是exception
我们要站在更高的角度,用比较抽象的方式来统一的总体的来处理一类问题.这是对AOP一个通俗的解释.
我们处理异常的时候,我们并不会把异常分散到具体的每一个业务代码中,
而我们提供了一个类似于横切面的东西,
这个横切面就是我们的ExceptionHandler.php下的render方法,它会统一的处理所有的异常.
举例:我们去看电影,电影院有一个检票口,
也许你的票在猫眼,美团...上买的.不管你在哪买的,
最后我们都要在检票口看你的票能不能入我的电影院.我们不能给每一个观影人都配一个检票员.
这个就是AOP思想的举例.
面向对象的3大特性: 封装性 继承性 多态性
java的spring框架的核心思想也是AOP思想.
D:\PanDownload\【微信小程序】\微信小程序商城构建全栈应用\第7章 数据库访问与ORM
7-1 数据库操作三种方式之原生
路由 版本好handler geturl 添加
最好还是路由
tp5
支持四种数据库
数据库连接 数据库设计思想->
G:\phpStudy\WWW\Zerg\thinkphp\library\think\db\connector
数据库驱动
//z.cn/api/v1/banner/1?调试* 鼠标停在数据库返回$result上 点加号 查看 结果
database.php 只需要配置下面就可以连接
Route::get('api/v1/banner/:id','api/v1.Banner/getBanner');
api/model\Banner
{
< ?php
namespace app\api\model;
use think\Db;
use think\Exception;
class Banner
{
public static function getBannerByID($id){
//TODO:根据Banner ID号 获取Banner信息 伪信息
// try{
// 1/0;//如果是变量 复杂 返回错误网页 客户端是json处理 则崩溃
// }catch (Exception $ex){
// //TODO:可以记录日志
// throw $ex;//往上抛 banner控制器
// }
// return 'this is banner info';
// return null;
$result=Db::query('select * from banner_item where banner_id=?',[$id]);
return $result;
//z.cn/api/v1/banner/1?调试* 鼠标停在数据库返回$result上 点加号 查看 结果
}
}
}
v1/banner/
{
< ?php
namespace app\api\controller\v1;
use app\api\validate\IDMustBePostiveInt;
use app\api\model\Banner as BannerModel;
use app\lib\exception\BannerMissException;
use think\Exception;// as 别名
class Banner
{
/**
* 获得指定id的banner信息
* @url /banner/:id
* @http GET
* @id banner的id号
* 方便以后增加多个banner
*/
public function getBanner($id)
{
//AOP面向切面编程
(new IDMustBePostiveInt())->goCheck();
$banner=BannerModel::getBannerByID($id);
if (!$banner){
// log('error');
throw new BannerMissException();// 自定义错误
// throw new Exception('内部错误');
}
//不做异常处理往上抛
// return $banner;
return json($banner);
}
}
}
7-2 从一个错误了解Exception的继承关系
路径不合法url banner1
异常俩同
HttpException 和不同子父关系 think\Exception
不同关系 不直接 不能自动
找他们共同的\Exception 根命名空间 -->
基类
ctrl=alt=o 无用命名空间use
ctrl=b 跳到声明处
base继承exception->不错
php官网手册
Exception是所有异常的基类 \think\Exception 继承它
\ php命名空间 -> thinkphp-->
解决错误 知道点 提升
找错误 信息 方法
-->步骤
use Exception 直接 Exception
ExceptionHandler类里面使用的render方法和recordErrorLog方法传入的是基类 \Exception
private function recordErrorLog(\Exception $e)
public function render(\Exception $e)
HttpException和think\Exception他们不是一个继承关系,所以不能自动的做类型转换.
找错误是一个很好的学习习惯
找错误就是一个寻宝的过程,错误信息就是藏宝图,你不断的发现信息查找信息.......
7-3TP5数据库中间层架构解析
tp5.0 手册 数据库 和模型
原生数据库
Db操作入口对象 查询 连接数据库
->实例化Collection连接不同的数据库 PO 执行sql语句才执行 节约资源云空间贵
Db->Drivers->Collection 根据驱动决定什么类型数据库
SQL
Query查询器 操作数据库语句 UI封装 连式操作 不同数据库sql不同 封装性的体现
->通过Builder生成器 编译生产原生sql语句 不同的Builer转换差异
->实例化Collection连接不同的数据库
OM模型
好像jq一样 浏览器的不同 统一标准方法 隐藏细节和差异
DAIL中间层
减少代码的好处
不关系sql代码,统一规范
跨数据库
框架平台
否则不够优雅代码多成本高 项目时间
thinkphp-thinkphp-fromwork
thinkphp db connector 连接各个数据库
thinkphp ab builder 出来不同数据库的sql
不同的类做不同的差异 aop
Db 是操作数据库的入口对象(换一种说话,我们增删改查都能通过DB来实现的)
Collection数据库连接器,并不是真正的连接数据库,而是起一种待命的状态.连接器是惰性的,好处是能够节约服务器的资源.它是执行sql语句的.
查询器其实最后也会被转化为原生的sql语句,相当于封装了sql,它翻译成原生的sql语句就是利用Builder生成器来实现的.一个大的作用,就是隐藏细节.支持不同数据库的查询.
drivers驱动 提供了几个不同的类 每一个类负责了一个不同的连接
总结:sql直接被Collection执行 Query被Builder翻译成sql,sql再被Collection执行.
设计模式的理解来源于日常的编码工作中的,当你遇见困难的时候再看去设计模式的书你才会茅塞顿开.23种设计模式不是死记硬背就可以的.
比较抽象
类拆分为Connection(连接器)/Query(查询器)/Builder(SQL生成器)
7-4查询构造器一
Query查询器 可以读写 方便 不同数据库差异
//table-where返回的是Query查询器的对象 find一维数组一条记录 select二维array多条
$result=Db::table('banner_item')->where('banner_id','=',$id)->select();
return $result;
断点走一步 完 才显示+...
分析错误变量 推理出 没捷径
Db::query对象 链式操作
使用query查询器来构造我们数据库的操作
7.4.1 为什么不使用原生的sql?
(1)原生的没有查询器简洁方便.
(2)最重要的是查询操作器封装了对不同数据库的操作.它提供了我们统一的数据库操作方法.不用关心不同数据库在原生sql语句的差异.所以这个是使用查询器最主要的原因.
query查询器不仅包含对数据库查询操作还具有写的操作,查询器名字只是一个泛写.因为不管是读写最终都会翻译成原生的sql语句.
在中小型的项目中还是推荐使用ORM
7-5查询构造器二
public static function getBannerByID($id){
//TODO:根据Banner ID号 获取Banner信息 伪信息
// try{
// 1/0;//如果是变量 复杂 返回错误网页 客户端是json处理 则崩溃
// }catch (Exception $ex){
// //TODO:可以记录日志
// throw $ex;//往上抛 banner控制器
// }
// return 'this is banner info';
// return null;
// $result=Db::query('select * from banner_item where banner_id=?',[$id]);
// return $result;
//z.cn/api/v1/banner/1?调试* 鼠标停在数据库返回$result上 点加号 查看 结果
//table-where返回的是Query查询器的对象 find一维数组一条记录 select二维array多条
//table-where辅助链式方法 可以多个 返回Query查询器的对象 不执行sql 不分先后 当相同的可以结果不一样
$result=Db::table('banner_item')->where('banner_id','=',$id)->select();
//等find select update delete insert 才执行sql 并且执行完 Db::清空 在select空name 之后俩个Db::不同
//非链式写法
// Db::table();
// Db::where();
// Db::select();
return $result;
}
//TODO: 根据id号, 获取Banner信息
//使用查询器不是直接返回的结果 而是返回的一个对象. 只有加上find函数,不过find()只能返回一条数据
//将find()替换为select()方法才可以返回所有查询的结果.
//find()返回的是一维数组 select()返回的是二维数组
//为什么称作为链式方法 因为它们像链子一样连在一起 但是链式方法不会真正去执行sql语句,需要类似于select find update delete insert
//类似的select() 还有 update() delete() insert() find()
//一运行select update delete insert find 以后 之前的 Db::....都被清除了
//where('字段名','表达式','查询条件'); where('banner_id','=',$id);
//where一共有3种写法. [表达式 , 数组法 , 闭包(最灵活的)]
//闭包写法
// $result = Db::table('banner_item')->where(function ($query) use ($id){
// $query->where('banner_id','=',$id);
// })->select();
//以下是原生的sql操作
//当执行Db的query方法时候,会自动将 $id 填充到 sql 语句内.
//$result = Db::query('select * from banner_item WHERE banner_id=?',[$id]);
//return $result;
系统支持的链式操作方法有:
{
连贯操作 作用 支持的参数类型
where* 用于AND查询 字符串、数组和对象
whereOr* 用于OR查询 字符串、数组和对象
wheretime* 用于时间日期的快捷查询 字符串
table 用于定义要操作的数据表名称 字符串和数组
alias 用于给当前数据表定义别名 字符串
field* 用于定义要查询的字段(支持字段排除) 字符串和数组
order* 用于对结果排序 字符串和数组
limit 用于限制查询结果数量 字符串和数字
page 用于查询分页(内部会转换成limit) 字符串和数字
group 用于对查询的group支持 字符串
having 用于对查询的having支持 字符串
join* 用于对查询的join支持 字符串和数组
union* 用于对查询的union支持 字符串、数组和对象
view* 用于视图查询 字符串、数组
distinct 用于查询的distinct支持 布尔值
lock 用于数据库的锁机制 布尔值
cache 用于查询缓存 支持多个参数
relation* 用于关联查询 字符串
with* 用于关联预载入 字符串、数组
bind* 用于数据绑定操作 数组或多个参数
comment 用于SQL注释 字符串
force 用于数据集的强制索引 字符串
master 用于设置主服务器读取数据 布尔值
strict 用于设置是否严格检测字段名是否存在 布尔值
sequence 用于设置Pgsql的自增序列名 字符串
failException 用于设置没有查询到数据是否抛出异常 布尔值
partition 用于设置分表信息 数组 字符串
}
7-6查询构造器三
where('字段名','查询条件')
= 缺略 不好
不区分大小写
//where(query对象 use传id)
// where('字段名','表达式','查询条件')
//表达式 数组法不够安全 闭包灵活
$result=Db::table('banner_item')->where(function ($query) use ($id){
$query->where('banner_id','=',$id); //可以继续where whereOr 但是不能用select...生成sql的
})->select();
//where一共有3种写法. [表达式 , 数组法 , 闭包(最灵活的)]
查询语法
where('字段名','表达式','查询条件');
whereOr('字段名','表达式','查询条件');
表达式不分大小写
{
表达式 含义
EQ、= 等于(=)
NEQ、<> 不等于(<>)
GT、> 大于(>)
EGT、>= 大于等于(>=)
LT、< 小于(<)
ELT、<= 小于等于(<=)
LIKE 模糊查询
[NOT] BETWEEN (不在)区间查询
[NOT] IN (不在)IN 查询
[NOT] NULL 查询字段是否(不)是NULL
[NOT] EXISTS EXISTS查询
EXP 表达式查询,支持SQL语法
> time 时间比较
< time 时间比较
between time 时间比较
notbetween time 时间比较
}
7-7开启SQL日志记录
工具debug
调试器下变量 key'*'
url ?XDEBUG_SESSION_START=*
sql相关
方法一
单条
//fetchSql() 返回sql语句 并没有真正执行
$result=Db::table('banner_item')->fetchSql()->where(function ($query) use ($id){
$query->where('banner_id','=',$id); //可以继续where whereOr 但是不能用select...生成sql的
})->select();
方法二
状态 信息全 多条
database.php
// 数据库调试模式
'debug' => true,
config.php
// 应用调试模式
'app_debug' => true,
'log' => [
// 日志记录方式,内置 file socket 支持扩展 test关闭自带
'type' => 'test',
// 日志保存目录
'path' => LOG_PATH,
// 日志记录级别
'level' => ['sql'],
],
测试关闭 所有要手动初始化 否则开启 file=test
如果在当php页 只能局部
最好在全url都要经过的入口文件设置
public/index.php
//开启SQL日志记录 全局 学习调试
\think\Log::init([
'type'=>'file',
'path'=> LOG_PATH,
'level'=>['sql']
]);
file=File 都行了
log
{
---------------------------------------------------------------
[ 2019-11-03T16:55:16+08:00 ] 127.0.0.1 GET z.cn/api/v1/banner/1?XDEBUG_SESSION_START=13884
[运行时间:0.106444s] [吞吐率:9.39req/s] [内存消耗:2,513.88kb] [文件加载:46]
[ sql ] [ DB ] CONNECT:[ UseTime:0.002049s ] mysql:host=127.0.0.1;port=3306;dbname=zerg;charset=utf8
[ sql ] [ SQL ] SHOW COLUMNS FROM `banner_item` [ RunTime:0.017211s ]
[ sql ] [ SQL ] SELECT * FROM `banner_item` WHERE ( `banner_id` = 1 ) [ RunTime:0.001174s ]
}
debug
sql日志记录 学习和调试才开
生产不 节约资源 云服务器自带全局
配置
(1)database.php下的 'debug' => true,
(2)config.php下的 'app_debug' => true,
(3)config.php下的
'log' => [
// 日志记录方式,内置 file socket 支持扩展
'type' => 'test',
// 日志保存目录
'path' => LOG_PATH,
// 日志记录级别
'level' => ['sql'],
],
(4)在index.php下添加
加这段的原因是我们在config.php下禁止了默认的日志,所以我们要在入口处初始化sql日志.
\think\Log::init([
'type' => 'File',
'path' => LOG_PATH,
'level' => ['sql']
]);
建议:一般在生产模式下,把sql的日志关闭.
每一个URL请求都过经过index.php文件
7-8ORM与模型
@ORM Obeject Relation Mapping 对象关系映射(用面对对象的思维去思考数据表)
用ORM我们尽量把每一个表看成是一个对象
ORM不是一种具体的语言也不是一种具体的框架,它也只是一种思想
举例:最著名的就是java里面的Hibernate就是ORM思想
@模型
//ORM Obeject Relation Mapping 对象关系映射
//模型 特指TP5的模型 在TP5中的模型是ORM实现的一个机制 它不仅是对数据库的查询 还包含了一些业务逻辑
//不要把模型想的太过单一了 把它想的大一点 不要仅仅理解为一个对象 业务的一个集合 也许很多对象合在一起也是一个模型
//模型主要是处理一些比较复杂的业务逻辑
//更直白的来说 ORM是把一个表当成一个对象来看待 但是模型可能是对应多个对象的 同时也可能对应对个表的
//这个模型中 表和对象没有必然的联系 模型是根据自己的业务逻辑而划分的-简单的来说就是根据自己的功能来划分的
//总结:不要把模型看做是数据库的查询 模型更多关注的是业务逻辑 不要把模型和数据库的表一一对应起来. 简单的业务逻辑对造成一个假象
//假象就是:一个模型对应一个数据表 但是复杂的模型和复杂的业务逻辑不是这样的 有可能横跨多个表 模型是个业务逻辑相关的
//模型不仅仅是model层 还有service .. 这2都是写业务逻辑的
7-9初识模型
config
// 默认输出类型 'default_return_type' => 'html',
'default_return_type' => 'json',
Module 返回信息全
Postman
header
不改:是 html
Content-Type →application/json; charset=utf-8
我们已经学会了2中数据库操作的方式
(1)原生SQL (2)Db
Db不是能很好的包含和处理我们的业务逻辑.
开始实现第一个model模型
继承Model类
model下的Banner.php
class Banner extends Model
这样Banner摇身一变就成为了一个模型
修改config.php下 将html修改为json
// 默认输出类型
'default_return_type' => 'json',
v1下的Banner.php
$banner = BannerModel::get($id); //返回的是模型对象 get方法是在BannerModel继承的Mode类里面定义的 我们不用手动去写getBannerByID
// $banner = BannerModel::getBannerByID($id);
为什么要用模型? 因为Banner继承了Model类,摇身一变为模型,在Model类中已经自动提供了get方法代替了自己写的getBannerByID方法.
//返回模型对象 一些方法操作 data
return $banner;//tp5自带的格式 config设置json格式
Db不会处理 登录不同ip提示异地登录 多端登录操作下线
继不承 都是模型 自定义方法 tp5带
7-10模型定义总结
模型都是继承Model这个类
业务逻辑简单的时候,一张表对应一个模型
在数据库里面有主表和从表这个概念的,TP5的关联模型对应数据库的主从表.
为什么调用get就知道去banner表去查询数据?
因为在默认情况下,数据库表的名字和我们的模型类名是一一对应的.
因此在调用get查询的时候TP5就知道该去哪个表去查询.但是表名字也不一定要和类名一样的,如果不想采用默认方式就需要在模型类下指明对应的哪一个数据库表.
只需要在对应的模型下添加 protected $table = 'banner_item';
class Banner extends Model
{
protected $table = 'banner_item';
}
快速创建模型的方法
TP5提供了一个自动生成模型的命令
(1)打开PHPStorm下的terminal(快捷键alt+f12)
(2)输入: php think make:model api/BannerItem (api为模块名 BannerItem对应的表名)
在根目录 下
(3)此刻model目录下就多出一个新的模型-BannerItem.php
7-11静态调用还是实例对象调用
TP5还是推荐静态调用的
//下面三句话 第一句是静态调用 后2句是实例对象调用 不过TP5推荐使用静态的调用方式
$banner = BannerModel::get($id); //返回的是模型对象 get方法是在BannerModel继承的Mode类里面定义的 我们不用手动去写getBannerByID
// $banner = new BannerModel();
// $banner = $banner->get($id);
为什么要使用静态调用?
(1)调用起来更加简洁
面向对象下的对象与类之间的关系?
(1)BannerModel对应的是数据库的一张表
(2)new 出来的实例对象对应的是表内的一条记录
(3)类是描述一个对象的相关属性和行为的.只有把类new出来之后,才能具体的代表一个事务.
(4)类可以看成是生产对象的一个模板
7-12 几种查询动词的总结与ORM性能问题的探讨
$banner=BannerModel::find($id);//静态方法
//模型都支持get find一条 all select多条 Db::支持find select
查询动词
//查询方式 get find all select (get find)只能返回一条数据 (all select)是返回的一组记录,或者说一组模型对象
//get all 是模型特有的方法 find select是Db特有的方法
//模型中也可以使用Db的方法 但是Db中不可以使用模型的get all方法
//为什么模型中也可以使用Db方法?因为Db是模型的基石 他们2者是不能分离的,在模型中最终访问的数据库还是使用数据库层的Db方法
@ORM为什么使用ORM?
1.我们编写代码很多时候就是解决现实世界所遇到的问题,编码很多时候就是现=现实世界事务的抽象.所以我能使用面向对象的思维去解决问题就使用面向对象的思维,
模型和数据库访问层是不同的2个概念,它们的职责是不同的.
模型主要是用来处理业务的.
而Db数据库访问层是用来查询数据库的.
但是模型是建立在Db的基础上的.
不要因为模型的性能较差就放弃使用模型
要用面向对象的思维去设计模型
模型的底层仍然是数据库访问抽象层
原则:好的代码第义原则是什么?
不能代码的性能,而是代码的可读性.
只有设计框架它们的性能就会超微差点,不能像c c++ 汇编语言一样,不过这种性能可以忽略,不如说我们的TP5,虽然性能是有损耗的,如果你使用一些非高级语言,以及不使用框架.那么你的开发效率和周期是有多长?那么你的性能损耗的价值和你的开发效率所损失的时间相比较起来,哦轻孰重?
如果你发现你的产品访问的很慢,真的是ORM引起的吗?
不要把访问速度慢直接就归因于ORM上,因为ORM所产生的性能问题客户是察觉不到的.
在经验来看,一般慢的原因都是sql语句写的不够好.
ORM其实没做什么,就是把原生的sql语句封装了一下.
如果项目比较大的时候,需要考虑的高并发的时候建议还是需要考虑使用原生的sql语句.但是绝大多情况的时候ORM就够了.
8-1 Banner相关表分析(数据表关系分析)
(1)banner 与 banner_item之间是依靠外键来联系到一起的.
(2)模型关联,我们就需要将banner与banner_item之间的表关联起来.它们之间的关联项就是banner_id,banner_id就是外键.
(3)banner表与banner_item之间是一对多的关系,就是一个banner可以对应多个banner_item.但是一个banner_item表只能对应一个banner表. 如果一个banner_item能对应多个banner表,那么它们之间就是多对多的关系.
banner_item与image之间是一对一的关系.
banner banner_item imgage 表之间都是靠外键联系在一起的.
banner 与 banner_item 是靠 banner_id关联在一起的
banner_item 与 image 是靠image_id 关联在一起的
8-2 模型关联----定于关联与查询关联
BannerItem.php sql banner_item
要在Banner模型里面表示 Banner模型与BannerItem模型之间的一对多的关系.
(学习模型关联,最重要的就是学会如何定义模型与模型之间的关联关系),
我们的目的是让Banner模型包含BannerItem模型
所以
Banner.php模型
public function items(){
//这不是一个普通的函数 我们将它成为 关联
//参数 第一个是关联模型的明星 第二个是外键 第三个是当前模型的主键
//当前模型是Banner 关联模型是BannerItem
//这里的$this就是Banner 调用下面的方法.
return $this->hasMany('BannerItem','banner_id','id');
}
Banner.php控制层
// $banner = BannerModel::find($id); //返回的是模型对象 get方法是在BannerModel继承的Mode类里面定义的 我们不用手动去写getBannerByID
//如果没有with就是查询的banner表 如果有with就是查询banner表和它所关联的表.
//解释这面一句话 BannerModel这个模型类 关联一个items 去找到id为1的banner
$banner = BannerModel::with('items')->find($id); //返回时的banner表的id记录 和对应的banner_item的记录.
返回json items数组
$this->hasMany('关联模型名','外键','当前主键') Banner模型包含BannerItem模型
class BannerItem extends Model
log
sql 多个字段 show columns from tp5要知道的信息 可关
8-3 模型关联----嵌套关联查询
模型关联-(嵌套查询)-如果banner banner_item还与其他的表有关联呢?
8.3.1 如果banner banner_item image之间都有关系如果一次性将所有的关联都查询出来?
8.3.2新建image模型
php think make:model api/Image
Image.php 查询关联谁 定义关联 <- ->
然后在BannerItem模型内创建与Image模型的关联模型
BannerItem .php模型
class BannerItem extends Model
{
public function img(){
//一对一之间的关系是用belongsTo
//foreignKey外键 localKey当前id主键 都是可以省略的,但是建议不要省略!!!
return $this->belongsTo('Image','img_id','id');
}
}
一对一:belongsTo
一对多:hasMany
最后在Banner控制层调用.
//with函数就是代表BannerModel模型内含有的,img没在BannerModel模型内,所以需要嵌套关系.
$banner = BannerModel::with(['items','items.img'])->find($id);
items.img嵌套
BannerItem.php
BannerModel.php fnItems fnItems1 with(['items','items1'])
返回页面 json 嵌套 数组
有一个疑问
为什么不在image模型中创建关联关系?
这样理解,当A模型调用B模型数据时,在A模型创建关联关系.
ORM模型的优势,
以为是sql语句日志,业务越来越复杂,
我们只需要写几句模型语句,就可以代替下列大量的sql语句.
{
[ 2018-02-05T13:18:00+08:00 ] ::1 ::1 GET /zerg/public/index.php/api/v1/banner/1?XDEBUG_SESSION_START=11139
[ log ] localhost/zerg/public/index.php/api/v1/banner/1?XDEBUG_SESSION_START=11139 [运行时间:0.659942s][吞吐率:1.52req/s] [内存消耗:2,667.41kb] [文件加载:44]
[ sql ] [ DB ] CONNECT:[ UseTime:0.003107s ] mysql:dbname=zerg;host=127.0.0.1;port=3306;charset=utf8
[ sql ] [ SQL ] SHOW COLUMNS FROM `banner` [ RunTime:0.015226s ]
[ sql ] [ SQL ] SELECT * FROM `banner` WHERE `id` = 1 LIMIT 1 [ RunTime:0.000967s ]
[ sql ] [ SQL ] SHOW COLUMNS FROM `banner_item` [ RunTime:0.010269s ]
[ sql ] [ SQL ] SELECT * FROM `banner_item` WHERE `banner_id` = 1 [ RunTime:0.000978s ]
[ sql ] [ SQL ] SELECT * FROM `banner_item` WHERE `banner_id` = 1 [ RunTime:0.001932s ]
[ sql ] [ SQL ] SHOW COLUMNS FROM `image` [ RunTime:0.020076s ]
[ sql ] [ SQL ] SELECT * FROM `image` WHERE `id` IN (65,2,3,1) [ RunTime:0.029929s ]
}
有舍有得
重次分明
8-4 隐藏模型字段
Model主要是编写业务逻辑.
隐藏模型字段意思就是客户端有时候不需要太多返回的信息,因此把原来很多的信息隐藏以后才返回给客户端.
删除Banner模型的delete_time字段
首先要明白数据返回的格式是一个对象
Banner.php的控制层
{
$banner = BannerModel::getBannerByID($id);
$banner->hidden(['delete_time','update_time']); //隐藏$banner下delete_time update_time属性
// $banner->visible(['id','items']); //只显示$banner下的id属性与items属性
//模型的好处就是提供了很多内置的方法,可以直接处理返回的数据
}
简单json 里面方法多
$banner=BannerModel::getBannerByID($id);
上可读性比下好 所以封装get-- 但不用原生 注释80%不用
$banner=BannerModel::with(['items','items.img'])->find($id);//静态方法
不用use
public static function getBannerByID($id){
当前 model banner BannerModel->self
$banner=self::with(['items','items.img'])->find($id);//静态方法
return $banner;
}
$banner=$banner->data;//ctrl+b protected只能本类本包用 在这是外 所以不能报错
\think\Medel
data() 是设置 不是获取
多了代码长 嵌套数组 改多
$data=$banner->toArray();//tp5toArray强大
unset($data['delete_time']);//多了代码长 嵌套数组 改多 unset过滤
return $data;
v1/banner
{
< ?php
namespace app\api\controller\v1;
use app\api\validate\IDMustBePostiveInt;
use app\api\model\Banner as BannerModel;
use app\lib\exception\BannerMissException;
use think\Exception;// as 别名
class Banner
{
/**
* 获得指定id的banner信息
* @url /banner/:id
* @http GET
* @id banner的id号
* 方便以后增加多个banner
*/
public function getBanner($id)
{
//AOP面向切面编程
(new IDMustBePostiveInt())->goCheck();
//返回模型对象 一些方法操作 data
//解释这面一句话 BannerModel这个模型类 with 关联一个items 去找到id为1的banner
// $banner=BannerModel::with('items')->find($id);//静态方法
// $banner=BannerModel::with(['items','items1'])->find($id);//静态方法
// with函数表BannerModel模型有的,img没BannerModel内,需嵌套关系.
// $banner=BannerModel::with(['items','items.img'])->find($id);//静态方法
// $banner=BannerModel::find($id);//静态方法
//模型都支持get find一条 all select多条 Db::支持find select
// $banner=new BannerModel();//实例化对象
// $banner=$banner->get($id);
$banner=BannerModel::getBannerByID($id);
// $banner=$banner->data;//ctrl+b protected只能本类本包用 在这是外 所以不能报错
// $data=$banner->toArray();//tp5toArray强大
// unset($data['delete_time']);//多了代码长 嵌套数组 改多 unset过滤
// return $data;
$banner=$banner->hidden(['delete_time','update_time']);//隐藏
// $banner=$banner->visible(['id','items']); 只显示
if (!$banner){
// log('error');
throw new BannerMissException();// 自定义错误
// throw new Exception('内部错误');
}
//不做异常处理往上抛
// return $banner;
// return json($banner);
return $banner;//tp5自带的格式 config设置json格式
}
}
}
model\Banner
{
< ?php
namespace app\api\model;
use think\Db;
use think\Exception;
use think\Model;
class Banner extends Model
{
//默认模型类型和数据库表名对应 不同得改
// protected $table='banner_item';
public function items(){
// $this->hasMany('关联模型名','外键','当前主键') Banner模型包含BannerItem模型
//hasMany一对多
return $this->hasMany('BannerItem','banner_id','id');
}
// public function items1(){}
public static function getBannerByID($id){
//不用use 当前 model banner BannerModel->self
$banner=self::with(['items','items.img'])->find($id);//静态方法
return $banner;
//TODO:根据Banner ID号 获取Banner信息 伪信息
// try{
// 1/0;//如果是变量 复杂 返回错误网页 客户端是json处理 则崩溃
// }catch (Exception $ex){
// //TODO:可以记录日志
// throw $ex;//往上抛 banner控制器
// }
// return 'this is banner info';
// return null;
// $result=Db::query('select * from banner_item where banner_id=?',[$id]);
// return $result;
//z.cn/api/v1/banner/1?调试* 鼠标停在数据库返回$result上 点加号 查看 结果
//table-where返回的是Query查询器的对象 find一维数组一条记录 select二维array多条
//table-where辅助链式方法 可以多个 返回Query查询器的对象 不执行sql 不分先后 当相同的可以结果不一样
// $result=Db::table('banner_item')->where('banner_id','=',$id)->select();
//等find select update delete insert 才执行sql 并且执行完 Db::清空 在select空name 之后俩个Db::不同
//非链式写法
// Db::table();
// Db::where();
// Db::select();
//where(query对象 use传id)
// where('字段名','表达式','查询条件')
//表达式 数组法不够安全 闭包灵活
//fetchSql() 返回sql语句 并没有真正执行Db::table('banner_item')->fetchSql()
// $result=Db::table('banner_item')->where(function ($query) use ($id){
// $query->where('banner_id','=',$id); //可以继续where whereOr 但是不能用select...生成sql的
// })->select();
//ORM Object Relation Mapping对象关系映射
//模型
// return $result;
}
}
}
8-5 在模型内部隐藏字段
对8.4隐藏字段做进步一的优化
隐藏字段名,本应让模型类自己管理的,因此去Model下的Banner去优化
只需要在Banner.php 模型类添加
protected $hidden = ['delete_time'];
//如果想要隐藏多个字段名,只需要 protected $hidden = ['delete_time','update_time'];
想要隐藏BannerItem表的字段名,只需要在BannerItem模型类下
隐藏字段的原因
(1)出于安全性的考虑,有一些的字段不应该返回到客户端的
(2)为了保证你返回客户端的json简洁.
//在控制器操作不好 在模型封装 内容多 items.id不行 很多接口返回 重复代码
$banner=$banner->hidden(['delete_time','update_time']);//隐藏
//隐藏客户端无用的 安全 简洁
//id 外键 不需要的隐藏
tp内部处理 不需要
'delete_time','update_time'
//from 1 本地 from 2网络图片 不需要hide
8-6 图片资源URL配置
本地相对路径最好
from 1 本地 from 2网络图片
图片地址不完整
config database route.php tp自动加载 不用use
可以在config后面追加 不建议
extra 自动加载 自己配置文件
setting.php 模仿config
'img_prefix'=>'http://z.cn/images' 地址域名变
图片放到 public目录下 只有这是公开访问的
public/images
读取自加载的extra
config('setting.img_prefix') 文件名.对象
直白写合并图片地址不好
定义方法类 不够好
最好还交到模型处理 tp5方法合并返回
图片资源的URL配置
在数据库的图片处配置相对路径
自己写一个配置文件,并且会被TP5框架自动加载,在application目录下新建一个extra的目录(这个目录会自动被TP5加载),然后在extra目录下新建一个php file名字为setting.
将images目录放到public目录下,因为在TP5中,只有public是一个公开的目录.
在setting.php下
return [
'img_prefix' => 'http://localhost/zerg/public/images'
];
于是在控制层 Banner.php下读取配置文件setting的信息
$c = config('setting.img_prefix');
接下来将数据库的url与我们基地址拼接
8-7 读取器的巧妙应用
控制器简洁
面向对象 是这个类是 在这个类完成
4次 数组 多
大写
断点学-框架
就是用于模型的读取器.
我们需要改模型Image的数据,因此我们就需要在Image模型内定义一个读取器.
Image.php
class Image extends Model
{
protected $hidden = ['delete_time','update_time','id','from'];
//这是一个读取器,名字命名是固定的 get + 字段名(大写) + Attr
//读取器可以接受一个参数
public function getUrlAttr($value){
return config('setting.img_prefix').$value;
}
}
为什么我们要在控制器能保持源代码的整洁?
因为如果我们有读源码的习惯,我们发现去读的时候都是一层一层去读的.如果我们将业务逻辑封装到类里面.那么我们读的时候就会顺.会比较的方便.
进一步优化,图片分为本地与网络,网络不需要拼接,因此需要作出判断from!=1时候不拼接
模型Image.php
public function getUrlAttr($value,$data){
//这里的$value $data都不是我们自己传入的参数,系统会自动给我们 可以设置断点查看.
$finalUrl = $value;
//判断from == 1 是判断图片是网络的还是本地的,当为1的时候是本地的,需要拼接url
if($data['from'] == 1) {
//在自己的配置文件读取基地址然后拼接url 然后就自动将url拼接完整.
$finalUrl = config('setting.img_prefix') . $value;
}
return $finalUrl;
}
关键是要学习好框架提供给你的工具.
其实这个读取器也是AOP实现的思想.
8-8 自定义模型基类
模型
什么时候触发读取器
$img=new Image();
$img->url;
关联 tp自动调用
$banner->item->image
其他框架读取器
tp叫获取器
其他有url..不行
提取封装函数
基类
url代表是路径
url其他 不一样
所以不读取器 get字段Attr
定义一个方法
子类再读取器 调用
业务代码逻辑 集中
子类自动继承方法
定义基类 完成共同
全部model 继承基类
在官方中,读取器叫做获取器.
读取器的触发是框架自动触发的,
当你使用了该模型就会调用该模型读取器并且关联的模型也会触发读取器.
如果其他表也有类似的url残缺字段.呢怎么办?现在我们将读取器写在Image模型下面的,如果我们其他的模型也有这个url字段呢.那么这个读取器就无法生效.我们将有更加面向对象的方式来处理这个问题.
我们创建一个新的BaseModel作为所有模型的基类,这个基类再继承Model
BaseModel.php
class BaseModel extends Model
{
//这里已经不是一个读取器了,因为如果是读取器那么所有含有url的字段都会被处理.我们只需要在用的时候再子类调用,
protected function prefixImgUrl($value,$data){
$finalUrl = $value;
if($data['from'] == 1) {
$finalUrl = config('setting.img_prefix') . $value;
}
return $finalUrl;
}
}
Image.php
//这样写法是需要url时候处理,调用相应的父类相应的方法就行.
public function getUrlAttr($value,$data){
return $this->prefixImgUrl($value,$data);
}
8-9 定义API版本号
产品经理 版本控制
//http://z.cn/api/v2/banner/1 http://z.cn/api/v1/banner/1
我们之所以能保证控制器的代码非常少,
这有赖于我们做了很好的面向对象的封装.
并且我们也利用了很多TP5自带的功能.从而才实现了代码这么简洁编写.
为什么我们要支持多版本?
我们开发的业务,但是业务不可能是一成不变的.当业务变得时候,我们如何能够更好的适应业务的变化.
我们拿getBanner做一个讲解.下面这个是我们大多数程序员写的,但是不提倡,我们应该新建一个v2目录.
public function getBanner($id,$version){
if(version==1){
******
}
if(version==2){
******
}
}
@开闭原则就是我们要对代码的扩展是要开放的.而对修改是封闭的.
我们经常面临是代码的改变,为了解决不断变化的代码.才产生了这么多的设计模式,以及设计思想.
那么开闭原则就是告诉大家你要修改一个代码.最好通过扩展的形式.
扩展形式exception就是很好的扩展原则.
尽量不要修改以前的代码,因为修改就会产生应该其他代码的风险.我们应该在其他的地方增加新的扩展.
优化
在controller目录下新建一个v2的目录
在v2中写类似于v1的
为什么要分版本号?
因为软件产品要兼容老版本,因为有些用户不会更新最新的版本号.关于版本的支持一直是一个很麻烦的事情.一定要在规划产品的时候考虑最多兼容几个版本.
route.php
//编写route是三段式 模块,控制器,操作方法
// api/v1.Banner/getBanner 不区分大小写
//Route::get('api/v1/banner/:id','api/v1.Banner/getBanner');
//因版本号不同作出相应的判断
Route::get('api/:version/banner/:id','api/:version.Banner/getBanner');
这就做到了不同版本之间相互不影响.
8-10 专题接口模型分析
命令行 控制器
php think make:controller api/v1/Theme
可更改 自定义模板
Theme 新s
controller
Theme
model
Product p di tou
Theme
数据库
多对多 最好 3表 2加字段也可以麻烦违法数据库开闭原则
theme->them_product->Product
分析theme表与product表之间关系
一个theme可以包含多个product
一个product可以属于多个theme
.因此theme与product之间是多对多之间的关系.
但是多对多之间的关系一般需要第三张表来表示
因此还有一个theme_product表.这个表做中转的关系表.
不是一定要有这个第三张表,但是没有这个表这个数据库设计是不好的.
不好的设计
就是在theme表字段内增添一个字段,
这个字段相当于一个数组,这个数组的内容就是该theme包含的product.
a#b#c#
虽然这种设计容易让人理解.这样写的不好之处就是
数据库的扩展性比较差,然后当你读取数据库相关字段的时候.
代码会写的非常麻烦. 举例
1#3#5#8
假如你现在专题1包含1,3,5,8产品
如果你想扩展你需要
1.先读取这个字段,
2.然后增加这个字段
3.最后更新.是很繁琐的.
这还是一个简单的业务逻辑.
当你想要统计这个专题下有多少个产品.
那么会更加比较麻烦.因此当多对多表的时候就要新建一张中转表.
多对多只建2张表,也违反了数据库设计思想.
因此theme product theme_product(中转表)构成了多对多之间的关系.
(1)开始编写theme控制器
使用命令来创建控制器类
php think make:controller api/v1/Theme
不过这里提倡自己手动创建,因为命令行创建很自动生成很多默认的方法.
因此Model可以使用命令行去创建.Controller适合自己手动去创建.
(2)创建Theme关联的模型类.
创建Theme模型类
创建Product模型类
但是不用创建ThemeProduct模型类 因为TP5框架内部会自动调用的.
不用创建中转表
Product.php 模型类
{
belongsTo('Image','topic_img_id','id');
}
public function headImg(){
return $this->belongsTo('Image','head_img_id','id');
}
}
为什么我们要在Theme.php模型定义关系关系,而不在Image.php模型类定义?
因为我们是通过Theme调用的,因此在该模型类下编写关联模型.
一对一之间也存在一个主从关系的.
8-12 Theme接口验证与重构
查询通常get
路由注释
@url
重构代码 方法 基类
Theme.php
class Theme
{
public function getSimpleList(){
}
}
接下来去定义它的路由.
route.php
Route::get('api/:version/theme','api/:version.Theme/getSimpleList');
控制层theme.php
class Theme
{
/**
* @url /theme?ids=id1,id2,id3...
* @return 一组theme模型
*/
public function getSimpleList($ids=''){
//验证传入的参数是否合法 因此去定义验证器.
}
}
接下来写参数验证器
在validate目录下新建 IDCollection.php 并且让它继承基类的验证器
'require|checkIDs'
];
//这里必须是$message 不能自己定义
//如果验证不通过返回的错误信息
protected $message = [
'ids' => 'ids必须是以为逗号分隔的多个正整数'
];
// $values 就是传入的参数ids
protected function checkIDs($value){
//将ids字符串转化为数组
$values = explode(',',$value);
//如果$values是空的 那么不符合要求
if(empty($values)){
return false;
}
//判断是否都是正整数
foreach ($values as $id){
if(!$this->isPositiveInteger($id)){
return false;
}
}
return true;
}
}
}
然后去测试, 我们做服务的人一定要有一个服务的心
在控制层Theme.php
class Theme
{
/**
* @url /theme?ids=id1,id2,id3...
* @return 一组theme模型
*/
public function getSimpleList($ids=''){
//验证传入的参数是否合法 因此去定义验证器.
(new IDCollection())->goCheck();
return 'success';
}
}
于是Theme参数的校验层就完成了.
IDMustBePostiveInt
isPositiveInteger 提取 重构 到基类 BaseValidate 做公共方法
protected $message=[
'id'=>'必须是正整数'
];
在自己错误信息
//z.cn/api/v1/theme/ids/1,2,3
///z.cn/api/v1/theme?ids=id1,id2,id3... 暂时不能加$debug代码
断点 单步 光标 方法进入
8-13 完成Theme简要信息接口
find() 返回的是一个对象
select() 一组数据集
返回模型数据集
{
goCheck();
//z.cn/api/v1/theme/ids/1,2,3
///z.cn/api/v1/theme?ids=id1,id2,id3... 暂时不能加$debug代码
$ids=explode(',',$ids); //字符串 to 数组 //接受参数,并将参数转换为数组的形式
//with模型和sql 关系
//find()一个对象 select()一组数据集
$result=ThemeModel::with('topicImg','headImg')->select($ids);
if(!$result){//返回没有抛异常
throw new ThemeException();
}
return $result;
// return 'success';
}
}
}
{
hasOne()在image用-无外键用 一对一 belongsTo有外键 关联对象表,当前关联外键,当前关联主键
return $this->belongsTo('Image','topic_img_id','id');
}
//Theme调用在 这关联模型 一对一主从关系
public function headImg(){
return $this->belongsTo('Image','head_img_id','id');
}
}
}
{
goCheck();
//接受参数,并将参数转换为数组的形式
$ids = explode(',',$ids);
$result = ThemeModel::with('topicImg,headImg')->select($ids);
return $result;
}
}
}
这样结果就能返回,下一步开始写 如果返回结果异常处理
在lib下的exception下创建ThemeException.php
{
belongsToMany(对象模型,中间表名,中间表关联对象外键,中间表当前主键) 多对多
同路由名
有一个路由匹配对,就不继续
配置config 路由完整匹配
// 路由使用完整匹配
'route_complete_match' => false, to true
v1/Theme
{
goCheck();
//z.cn/api/v1/theme/ids/1,2,3
///z.cn/api/v1/theme?ids=id1,id2,id3... 暂时不能加$debug代码
$ids=explode(',',$ids); //字符串 to 数组 //接受参数,并将参数转换为数组的形式
//with模型和sql 关系
//find()一个对象 select()一组数据集
$result=ThemeModel::with('topicImg','headImg')->select($ids);
if(!$result){//返回没有抛异常
throw new ThemeException();
}
return $result;
// return 'success';
}
//标题图片那个 http://z.cn/api/v1/theme/1
public function getComplexOne($id){
return 'success';
}
}
}
route.php
Route::get('api/:version/theme','api/:version.Theme/getSimpleList');
//同路由名
//有一个路由匹配对,就不继续 配置config 路由完整匹配
Route::get('api/:version/theme/:id','api/:version.Theme/getComplexOne');
model\Theme
{
hasOne()在image用-无外键用 一对一 belongsTo有外键 关联对象表,当前关联外键,当前关联主键
return $this->belongsTo('Image','topic_img_id','id');
}
//Theme调用在 这关联模型 一对一主从关系
public function headImg(){
return $this->belongsTo('Image','head_img_id','id');
}
//多对多
public function products(){
// $this->belongsToMany(对象模型,中间表名,中间表关联对象外键,中间表当前主键) 多对多
return $this->belongsToMany('Product','theme_product','product_id','theme_id');
}
}
}
开启路由完整匹配模式这个很重要
需要在config.php修改route_complete_match属性为true
// 路由使用完整匹配
'route_complete_match' => true,
8-15 编写Theme详情接口
不要模糊的一对一
v1/Theme
{
goCheck();
//z.cn/api/v1/theme/ids/1,2,3
///z.cn/api/v1/theme?ids=id1,id2,id3... 暂时不能加$debug代码
$ids=explode(',',$ids); //字符串 to 数组 //接受参数,并将参数转换为数组的形式
//with模型和sql 关系
//find()一个对象 select()一组数据集
$result=ThemeModel::with('topicImg','headImg')->select($ids);
if(!$result){//返回没有抛异常
throw new ThemeException();
}
return $result;
// return 'success';
}
//标题图片那个 http://z.cn/api/v1/theme/1
/**
* @url /theme/:id
*/
public function getComplexOne($id){
//校验数字正整数 已经oop
(new IDMustBePostiveInt())->goCheck();
$theme=ThemeModel::getThemeWithProducts($id);
if(!$theme){
throw new ThemeException();
}
return $theme;
}
}
}
model\Theme
{
hasOne()在image用-无外键用 一对一 belongsTo有外键 关联对象表,当前关联外键,当前关联主键
return $this->belongsTo('Image','topic_img_id','id');
}
//Theme调用在 这关联模型 一对一主从关系
public function headImg(){
return $this->belongsTo('Image','head_img_id','id');
}
//多对多
public function products(){
// $this->belongsToMany(对象模型,中间表名,中间表关联对象外键,中间表当前主键) 多对多
return $this->belongsToMany('Product','theme_product','product_id','theme_id');
}
//建立关联模型关系 topicImg 保证资源的完整性 with居于资源的字符串 数组
public static function getThemeWithProducts($id){
$theme=self::with('products,topicImg,headImg')->find($id);
return $theme;
}
}
}
8-16 数据库字段冗余的合理利用
数据表json 返回
'pivot' tp5自带返回 多对多 的 中间表
数据多余 意义重复
'main_img_url'
'img_id'
model\Product
{
prefixImgUrl($value,$data);
}
}
}
有个度
为上一节补充.为什么不推荐滥用数据冗余,对数据的完整性,以及一致性的维护来说是非常困难的
比如,
(1)当你写入数据的时候,你就需要同时对2处作出一样的操作.
(2)最大的问题在于删除和更新的时候,特别是更新的时候,
一个地方改变对应的也要改变.如果忘记就会产生数据的不一致,
REST是基于资源的.
8-18 最近新品接口编写
查询 倒序
显示最新15
数据库
product
tp5 自动调用 存的
模型操作 才有这个功能
delete_time create_time update_time
delete_time 标识 假删除
create_time 使用模型插入才用值
数据库导入没有
只能模拟 插入时间戳
没有时 排序 安装id 主键
一些null 一些有时间戳 先排序有值的
validate
{
'isPostiveInt|between:1,15'
];
//不写默认tp5
// protected $message=[
// 'count'=>['isPostiveInt'=>'必须是正整数','between'=>'1-15']
// ];
}
}
model
{
prefixImgUrl($value,$data);
}
//关联数据模型 不用with 一条记录
public static function getMostRecent($count){
//limit获取一个 order new 倒序 一组数据 order('create_time desc')也行
//create_time 使用模型插入才用值
//数据库导入没有
//只能模拟 插入时间戳
//
//没有时 排序 安装id 主键
//
//一些null 一些有时间戳 先排序有值的
$products=self::limit($count)->order('create_time','desc')->select();
return $products;
}
}
}
exception
{
goCheck();
$products=ProductModel::getMostRecent($count);
if (!$products){
throw new ProductException();
}
return $products;
}
}
}
路由
Route::get('api/:version/product/recent','api/:version.Product/getRecent');
控制器
validate model exception
最近新品就是product表,根据上传数据库的时间倒序排列选出15条.
详细在控制层Product.php
8-19 使用数据集还是数组?
tp5 字符串 不要随便加空格 源码不做空格处理
? & debug 都可以
$collection 一组数据模型 不是数组
hidden 临时隐藏不影响模型 其他调用
要模型直接返回 就是数据集 去 database.php设置
// 数据集返回类型 默认array
'resultset_type' => 'collection',
http://z.cn/api/v1/theme?ids=4
[]
异常处理不行
不能!var 空
var->isEmpty()
全部 v1 控制器 判断改
// if(!$var) 改模型返回collection []空 isEmpty
特别提醒, 在TP5的字符串中,不要随便写空格!!!
使用数据集临时隐藏字段
Product.php
{
public function getRecent($count=15){
//1.方法定义完去route.php 定义路由
//2.如果客户端没有传入参数 默认就为15
//3.参数校验.
//4.编写模型getMostRecent()
//5.手动将模型引入进来 use app\api\model\Product as ProductModel;
//6.编写异常 ProductException.php
//7.ok
(new Count())->goCheck();
$products = ProductModel::getMostRecent($count);
if(!$products){
throw new ProductException();
}
//$collection是一个数据集它有一个默认的方法能够解决 临时隐藏字段的 函数
$collection = collection($products);
//临时隐藏字段
$products = $collection->hidden(['summary']);
return $products;
}
}
比如有一组数组,我们需要对它每一个做相同的处理.比较麻烦,TP5提供了一个数据集,将数组封装成一个collection对象,在对象上执行方法会对每一个数组item进行处理.
优化
在database.php下修改 将array改成collection
// 数据集返回类型
'resultset_type' => 'collection',
Product.php 优化过的,因为在database.php下直接设置了返回的是数据集而不是数组.
{
public function getRecent($count=15){
//1.方法定义完去route.php 定义路由
//2.如果客户端没有传入参数 默认就为15
//3.参数校验.
//4.编写模型getMostRecent()
//5.手动将模型引入进来 use app\api\model\Product as ProductModel;
//6.编写异常 ProductException.php
//7.ok
(new Count())->goCheck();
$products = ProductModel::getMostRecent($count);
if(!$products){
throw new ProductException();
}
//临时隐藏字段
$products = $products->hidden(['summary']);
return $products;
}
}
不过上面的代码已经有bug了
if(!$products)是错误的了,因为返回的是数据集了,用!已经不起作用了,只能用isEmpty();
if($products->isEmpty())
8-20 分类列表接口编写
隐式外键 tp5弄
真的 外键 数据库里关联
简单不在模型封装了
关联模型关系
model::with('img')->select($id);
==
model::all([],'img');
//不传参数 不用校验
v1
{
select($id);
// ==
// model::all([],'img');
//http://z.cn/api/v1/category/all
//不传参数 不用校验
$categories=CategoryModel::all([],'img');
// $categories=CategoryModel::getAll();
if ($categories->isEmpty()){
throw new CategoryException();
}
return $categories;
}
}
}
model
{
belongsTo('Image','topic_img_id','id');
}
//如果在这封装
public static function getAll(){
$categories=self::all([],'img');
return $categories;
}
}
}
Exception
{
goCheck();
$products=ProductModel::getMostRecent($count);
if ($products->isEmpty()){
throw new ProductException();
}
//$collection 一组数据模型 方法 不是数组 hidden 临时隐藏不影响模型 其他调用
//要模型直接返回 就是数据集 去 database.php设置 resultset_type
// $collection=collection($products);
// $products=$collection->hidden(['summary']);
$products=$products->hidden(['summary']);
return $products;
}
//列表详细内容 Category分类 z.cn/api/v1/product/by_category?id=2 /id/1
public function getAllInCategory($id){
(new IDMustBePostiveInt())->goCheck();
$products=ProductModel::getProductsByCategoryID($id);
//同样Product
if ($products->isEmpty()){
throw new ProductException();
}
$products=$products->hidden(['summary']);
return $products;
}
}
}
{
prefixImgUrl($value,$data);
}
//关联数据模型 不用with 一条记录
public static function getMostRecent($count){
//limit获取一个 order new 倒序 一组数据 order('create_time desc')也行
//create_time 使用模型插入才用值
//数据库导入没有
//只能模拟 插入时间戳
//
//没有时 排序 安装id 主键
//
//一些null 一些有时间戳 先排序有值的
$products=self::limit($count)->order('create_time','desc')->select();
return $products;
}
public static function getProductsByCategoryID($categoryID){
//查询对应列表的 详细页信息
$products=self::where('category_id','=',$categoryID)->select();
return $products;
}
}
}
Route::get('api/:version/product/by_category','api/:version.Product/getAllInCategory')
url
http://localhost/zerg/public/index.php/api/v1/product/by_category?id=5
控制层Product.php下的getAllInCategory
D:\PanDownload\【微信小程序】\微信小程序商城构建全栈应用\第9章微信登陆与令牌
9-1初识T--意义与作用
之前我们做的所有接口都没有权限控制,所有用户都可以调用.
但是有些接口能公开访问.有些接口就不能公开访问.这个权限怎么做?
我们是用令牌管理我们的用户身份的.登录就是获取一个令牌.用户在每一次api请求都需要携带它的令牌.
下面我们逐步学习令牌的设计以及原理.
获取令牌
客户端-user|pw->getToken接口(同Banner)<->帐号密码Token Auth
上面的流程是一个通用的设计模式.
访问接口
客户端-携带Token->下单接口<->帐号密码Token Auth
Auth代表身份 Token也有过期时间 不是有Token就能获取下单接口,还有验证身份..........
(1)验证Token是否合法
(2)验证是否Token有效(过期没)
(3)验证Token所对应的权限
9-2微信身份体系设计
统一个身份一个一个账号用微信什么u id
openid session_key
安全不返回客户端
可以客户端缓存 不用
openid 不会失效 要有效的token 返回
不存数据库 压力大
用缓存 人多访问读取多
上节是将的基础,思路,这一节是对上面的细化
微信服务器^vopenid.session_key
小程序-Code-><-返回Token-getToken----openid-->生成Token,存储记录openid
实际上将Token和用户信息存储在缓存中,加快访问速度
因为小程序会为每一个用户生成一个Code码,因此不再需要传递账户,密码.只需将Code码传递给getToken,
在getToken方法中我们需要将Code码传递到微信服务器中,服务器会返回你2个重要的信息(openid,session_key),
openid就是用户唯一的标识
不过还有别的用处,
比如微信支付时候.在我们这里不需要使用session_key.同一个用户在不同的小程序内部openid是不同的.
在getToken方法下拿到Token,然后将Token存起来.于是用户身份体系就建立起来了.
然后将Token返回给客户端,让客户端每次访问都携带Token.
不建议将openid传入到客户端,因为openid安全的原因,还有openid不能改变.就不能设置它的有效期.解决方法就是生成一个令牌.
缓存实际上将Token和用户信息存储在缓存中,
加快访问速度.但是不要滥用缓存.虽然缓存用起来的非常简单的,
但是缓存的维护是非常麻烦的.这个远远要比数据库的维护还要麻烦.
^缓存
小程序-token->校验Token->下单接口
缓存的好处是不用去查数据库了,直接去缓存内校验Token
9-3实现T身份权限体系
LoadToken 动作
Token 资源
路由
user Token 第三方管理
require 不能校验空 自定义
设置在基类里
用户信息存 sql user表 openid
service 复杂逻辑
UserToken 业务类
模型 简单业务 数据库CUDI
学成自己
v1
{
goCheck();//校验参数 去目录validate
$ut=new UserToken();//service
$token=$ut->get($code);
return $token;
}
}
}
model
{
'require|isNotEmpty'
];
protected $message=[
'code'=>'没有code还想获取Token,做梦吧'
];
}
}
父
{
param();//获得所有的参数
$result=$this->batch()->check($params);//继承Validate 在内部不用new
if (!$result){
$e=new ParameterException([
'msg'=>$this->error,
// 'code'=>400,
// 'errorCode'=>10002
]);
// 俩种赋值都行了 但是上面的比较符合面向对象
// $e->msg=$this->error;//具体错误信息
// $e->errorCode=10002;
throw $e;
//失败时干什么 把错误信息返回 用户那错误
//getEttor()获取错误信息 这是调用设置 不用get
// $error=$this->error;
//抛出异常 把访问中断
//其实到时自定义异常 错误信息不容易看 信息安全...
// throw new Exception($error);//tp5默认异常
}
else{
return true;//成功返回true
}
}
protected function isPostiveInt($value,$rule='',$data='',$field=''){
//先判断是不是数字 是不是整数 大于0的数(不是0或者负数)
if (is_numeric($value)&&is_int($value+0)&&($value+0)>0){
return true;
}
else{
return false;
// return $field.'必须是正整数';//自己类是message
}
}
protected function isNotEmpty($value,$rule='',$data='',$field=''){
if (empty($value)){
return false;
}else{
return true;
}
}
}
}
}
route
Route::post('api/:version/token/user','api/:version.Token/getToken');
route.php
//获取Token 为什么这里要用post
因为code安全性
get参数只能放在url路径下,
因此用post将参数放到body里面去.比get要好一点.
但是使用抓包工具还是能查看到相应的信息.
因此最安全的是https
Route::post('api/:version/token/user','api/:version.Token/getToken');
因为开始编写User模型业务逻辑就开始更加麻烦,因此我们再写一个service层.
service层很好的体现了MVC的Model层的分层概念.
Service就是处理较为复杂的业务逻辑.
而Model只是处理一些细粒度的单个业务逻辑.
Model层不仅编写简单的业务逻辑,而且对数据库操作.实现数据表的增删改查.
Model模型定义的名字必须与数据库的表名相一致!
编写Token
控制层Token.php
class Token
{
//code是小程序用户都会拥有的
public function getToken($code=''){
//校验参数 去目录validate
//编写路由
(new TokenGet())->goCheck();
$ut = new UserToken();
$token = $ut->get($code);
return $token;
}
}
模型层User.php
class User extends BaseModel
{
}
Service层下UserToken.php
class UserToken
{
public function get($code){
}
}
路由route.php
//获取Token 为什么这里要用post 因为code安全性 get参数只能放在url路径下,因此用post将参数放到body里面去
Route::post('api/:version/token/user','api/:version.Token/getToken');
9-4实现TOKEN身份权限体系二--获取enid
https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/wx.login.html
https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/login/auth.code2Session.html
https://mp.weixin.qq.com/
appid 小程序唯一标识 wx3ce92e92af9b29b1
secret 小程序app secret 4ceea0beecb10a88e79368e7df9a12dc
js_code 登录时获取的code
grant_type=authorization_code 默认
请求地址
https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
经验 多次尝试
微信返回 不是标准rest
success fail 都200
extra
{
wx.php
'wx3ce92e92af9b29b1',
'app_secret'=>'4ceea0beecb10a88e79368e7df9a12dc',
// %s 为占位符
'login_url'=>' https://api.weixin.qq.com/sns/jscode2session?'.
'appid=%s&secret=%s&js_code=%s&grant_type=authorization_code'
];
}
service
{
$code=$code;
$this->wxAppID=config('wx.app_id');
$this->wxAppSecret=config('wx.app_secret');
//拼接完整url %s 格式
$this->wxLoginUrl=sprintf(config('wx.login_url'),$this->wxAppID,$this->wxAppSecret,$this->code);
}
public function get(){
$result=curl_get($this->wxLoginUrl);
$wxResult=json_decode($result,true);//字符串转数组
if (empty($wxResult)){//empty比较好用
throw new Exception('获取session_key及openID时异常,微信内部错误');//内部错误 tp5自带的 外部问题
}else{
//错误返回是有errcode 判断存在
$loginFail=array_key_exists('errcode',$wxResult);
if ($loginFail){
}else{
}
}
}
}
}
// 应用公共文件
common.php
{
// +----------------------------------------------------------------------
// 应用公共文件
/**
* @param string $url get请求地址
* @param int $httpCode 返回状态码
* @return mixed
* 封装的http 请求
*/
function curl_get($url,&$httpCode = 0){
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
//不做证书校验,部署在linux环境下请改为true
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,false);
curl_setopt($ch,CURLOPT_CONNECTTIMEOUT,10);
$file_contents = curl_exec($ch);
$httpCode = curl_getinfo($ch,CURLINFO_HTTP_CODE);
curl_close($ch);
return $file_contents;
}
}
现在我们要通过小程序向控制层Token接口的getToken方法请求,发送code.
9.4.1写自定义的配置文件,在extra目录下的新建一个php file.名为wx
wx.php
{
return [
//app_id app_secret为自己小程序申请时拥有的.
'app_id' => '************',
'app_secret' => '*****************',
// %s 为占位符
'login_url' => 'https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code',
];
}
开始编写http请求,因为很多地方都要拥有http请求,
因此将该方法写成公共方法,
TP5提供了一个地方能够使TP5所有的类都使用,
在application下的common.php
{
// 应用公共文件
/**
* @param string $url get请求的地址
* @param int $httpCode 返回状态码
* @return mixed
*/
function curl_get($url,&$httpCode = 0){
$ch = curl_init();
curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt($ch,CURLOPT_RETURNTRANSFER,1);
//不做证书校验,部署在linux环境下请改为true
curl_setopt($ch,CURLOPT_SSL_VERIFYPEER,false);
curl_setopt($ch,CURLOPT_CONNECTTIMEOUT,10);
$file_contents = curl_exec($ch);
$httpCode = curl_getinfo($ch,CURLINFO_HTTP_CODE);
curl_close($ch);
return $file_contents;
}
}
wx.login(Object object)
调用接口获取登录凭证(code)。
通过凭证进而换取用户登录态信息,
包括用户的唯一标识(openid)及本次登录的会话密钥(session_key)等。
用户数据的加解密通讯需要依赖会话密钥完成。
用户登录凭证(有效期五分钟)。
开发者需要在开发者服务器后台调用 auth.code2Session,使用 code 换取 openid 和 session_key 等信息
登录凭证校验。
通过 wx.login 接口获得临时登录凭证 code 后传到开发者服务器调用此接口完成登录流程。
请求地址
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
{
属性 类型 默认值 必填 说明
appid string 是 小程序 appId
secret string 是 小程序 appSecret
js_code string 是 登录时获取的 code
grant_type string 是 授权类型,此处只需填写 authorization_code
}
返回的 JSON 数据包
{
属性 类型 说明
openid string 用户唯一标识
session_key string 会话密钥
unionid string 用户在开放平台的唯一标识符,在满足 UnionID 下发条件的情况下会返回,详见 UnionID 机制说明。
errcode number 错误码
errmsg string 错误信息
}
errcode 的合法值
值 说明 最低版本
-1 系统繁忙,此时请开发者稍候再试
0 请求成功
40029 code 无效
45011 频率限制,每个用户每分钟100次
9-5实现T身份权限体系三
code 一次 5分钟
Posman
body
form-data
key-value {"code":"微信获取的code"}
code:000
raw 推荐
{"code":"微信获取的code"}
code
Tests json
工具填自己id
prototss-tool
https://coding.imooc.com/class/97.html?mc_marking=b6fd1ffa6f664a3259d6ff0ddc534067&mc_channel=shouji
expres_in:7200 时间改小
一边写一边调
工具没打过 群不行麻烦过
https://api.weixin.qq.com/sns/jscode2session?appid=wx3ce92e92af9b29b1&secret=4ceea0beecb10a88e79368e7df9a12dc&js_code=043hY3Zi0AdOWp1xM2Xi00n6Zi0hY3Zp&grant_type=authorization_code
https://www.php.cn/toutiao-395857.html
curl请求curl_exec返回false,curl_error返回空
网上查找了一下,由于采用https协议,一定要加入以下两句
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); //不验证证书下同
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); //
Protocol " https" not supported or disabled in libcurl
libcurl中不支持或禁用协议“https”
PHP curl_exec函数
思路一
https://blog.csdn.net/testcs_dn/article/details/82529023
只需要把单引号改为双引号就可以了。
二:
http://blog.okbase.net/JO2000/archive/55957.html
指定SSL版本
curl_setopt($ch, CURLOPT_SSLVERSION, 6);
三:
调试方法
https://www.php.cn/php-weizijiaocheng-362493.html
在curl_exec 后加
$val=curl_error($ch); 调试 url 空格 只能双引号
Protocol " https" not supported or disabled in libcurl
CURLOPT_RETURNTRANSFER选项被设置,函数执行成功时会返回执行的结果,失败时返回 FALSE 。
https://www.runoob.com/php/func-curl_exec.html
common.php
{
// +----------------------------------------------------------------------
// 应用公共文件
/**
* @param string $url get请求地址
* @param int $httpCode 返回状态码
* @return mixed
* 封装的http 请求
*/
function curl_get($url, &$httpCode = 0)
{
$ch = curl_init();//init
curl_setopt($ch, CURLOPT_URL, $url);//url
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);//1 true 返回结果 0-truefalse
// curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); http版本
//不做证书校验,部署在linux环境下请改为true
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
// curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); ssl host
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 120);//超时10=10s
$file_contents = curl_exec($ch);//执行
// $val=curl_error($ch); 调试 url 空格 只能双引号
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);//函数获取CURL请求输出的相关信息
curl_close($ch);//关闭释放资源
return $file_contents;
}
}
wx.php
url "" 不能空格
{
'wx3ce92e92af9b29b1',
'app_secret'=>'4ceea0beecb10a88e79368e7df9a12dc',
// %s 为占位符
'login_url'=>"https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
];
}
工具 获取 openid测试
{
//index.js
//获取应用实例
const app = getApp()
// var baseUrl='http://zerg.cn/api/v1';
//只能用一次 5fz 不wxtool posman
Page({
data:{
},
getToken: function () {
//调用登录接口
wx.login({
success: function (res) {
var code = res.code;
console.log('code');
console.log(code);
wx.request({
url: baseUrl + '/token/user',
data: {
code: code
},
method: 'POST',
success: function (res) {
console.log(res.data);
wx.setStorageSync('token', res.data.token);
},
fail: function (res) {
console.log(res.data);
}
})
}
})
},
})
}
Service
{
code=$code;
$this->wxAppID=config('wx.app_id');
$this->wxAppSecret=config('wx.app_secret');
//拼接完整url %s 格式
$this->wxLoginUrl=sprintf(config('wx.login_url'),$this->wxAppID,$this->wxAppSecret,$this->code);
}
public function get(){
$result=curl_get($this->wxLoginUrl);// bebug
$wxResult=json_decode($result,true);//字符串转数组
if (empty($wxResult)){//empty比较好用
throw new Exception('获取session_key及openID时异常,微信内部错误');//内部错误 tp5自带的 外部问题
}else{
//错误返回是有errcode 判断存在
$loginFail=array_key_exists('errcode',$wxResult);
if ($loginFail){
$this->processLoginError($wxResult);
}else{
$this->grantToken($wxResult);
}
}
}
private function grantToken($wxResult){
//拿到openid
//数据库里看一下,这个openid是不是已经存在
//如果存在 则不处理,如果不存在那么新增一条user记录 sql user表 openid字段
//生成令牌,准备缓存数据,写入缓存
//把令牌返回到客户端去
$openid=$wxResult['openid'];// bebug
}
//定义错误处理 WeChatException
private function processLoginError($wxResult){
//错误返回客户端 根据情况定
throw new WeChatException([
'msg'=>$wxResult['errmsg'],
'errorCode'=>$wxResult['errcode']
]);
}
}
}
V1
{
goCheck();//校验参数 去目录validate bebug
$ut=new UserToken($code);//service
$token=$ut->get();// bebug
return $token;
}
}
}
Exception
{
find();
return $user;
}
}
}
service
{
code=$code;
$this->wxAppID=config('wx.app_id');
$this->wxAppSecret=config('wx.app_secret');
//拼接完整url %s 格式
$this->wxLoginUrl=sprintf(config('wx.login_url'),$this->wxAppID,$this->wxAppSecret,$this->code);
}
public function get(){
$result=curl_get($this->wxLoginUrl);// bebug
$wxResult=json_decode($result,true);//字符串转数组
if (empty($wxResult)){//empty比较好用
throw new Exception('获取session_key及openID时异常,微信内部错误');//内部错误 tp5自带的 外部问题
}else{
//错误返回是有errcode 判断存在
$loginFail=array_key_exists('errcode',$wxResult);
if ($loginFail){
$this->processLoginError($wxResult);
}else{
$this->grantToken($wxResult);
}
}
}
private function grantToken($wxResult){
//拿到openid
//数据库里看一下,这个openid是不是已经存在
//如果存在 则不处理,如果不存在那么新增一条user记录 sql user表 openid字段
//生成令牌,准备缓存数据,写入缓存
//把令牌返回到客户端去
//缓存key:令牌
//value: wxResult,uid用户主键,scope权限
$openid=$wxResult['openid'];// bebug
//静态方法静态调用
$user=UserModel::getByOpenID($openid);
// //如果存在,则不处理,如果不存在,那么新增一个user记录
if ($user){
$uid=$user->id;//如果存在 把id返回
}else{
$uid=$this->newUser($openid);//没有创建//使用模型插入数据
}
//生成令牌,准备缓存数据,写入缓存
//缓存key:令牌
//value:wxRelsut(sessionkey openid);uid,scope(决定用户身份)
$cachedValue=$this->prepareCachedValue($wxResult,$uid);
}
private function saveToCache($cachedValue){//缓存Key Token
$key=generateToken();//令牌随机字符串
}
////准备缓存value的数据
private function prepareCachedValue($wxResult,$uid){//缓存value wxResult,uid用户主键,scope权限e
$cachedValue=$wxResult;
$cachedValue['uid']=$uid;
$cachedValue['scope']=16;//数值越大 权限也大 暂时
return $cachedValue;
}
private function newUser($openid){//没有创建//插入数据函数
$user=UserModel::create(['openid'=>$openid]);//调用模型的create()
return $user->id;
}
//定义错误处理 WeChatException
private function processLoginError($wxResult){
//错误返回客户端 根据情况定
throw new WeChatException([
'msg'=>$wxResult['errmsg'],
'errorCode'=>$wxResult['errcode']
]);
}
}
}
用户登录、权限分级与接口保护对于API来说是非常重要的。我们将使用微信的身份认证体系来实现免密登陆、使用Token令牌来替代我们传统Web开发中的Cookie进行用户身份验证与权限分级。在有了Token令牌后,我们就可以对用户相关接口:收货地址进行编写了
2、告诉curl,请求的地址
// 经验总结得:如果返回的结果为空[没有返回错误信息和错误代码],则是微信服务器接口的问题,直接抛出异常一颗
// 程序传递的参数出错时,微信服务器会返回错误码和错误提示信息
成功获取微信接口返回数据后的操作[存储 openid、生成令牌、写入缓存、返回令牌]
[微信返回数据(openid|session_key) + uid(用户服务器中保存的用户记录 id) + scope(用户权限,值越大,权限越高) ]
9-7实现T身份权限体系五
技巧
$this 类里面
直接 可能common
尽力在类里面
调用基类 和 自己 方法
self::
数组转字符串 json格式
json_encode
service
{
private function grantToken($wxResult){
//拿到openid
//数据库里看一下,这个openid是不是已经存在
//如果存在 则不处理,如果不存在那么新增一条user记录 sql user表 openid字段
//生成令牌,准备缓存数据,写入缓存
//把令牌返回到客户端去
//缓存key:令牌
//value: wxResult,uid用户主键,scope权限
$openid=$wxResult['openid'];// bebug
//静态方法静态调用
$user=UserModel::getByOpenID($openid);
// //如果存在,则不处理,如果不存在,那么新增一个user记录
if ($user){
$uid=$user->id;//如果存在 把id返回
}else{
$uid=$this->newUser($openid);//没有创建//使用模型插入数据
}
//生成令牌,准备缓存数据,写入缓存
//缓存key:令牌
//value:wxRelsut(sessionkey openid);uid,scope(决定用户身份)
$cachedValue=$this->prepareCachedValue($wxResult,$uid);
$token=$this->saveToCache($cachedValue);
return $token;//写入缓存,并返回令牌
}
private function saveToCache($cachedValue){//缓存Key Token
$key=self::generateToken();//令牌随机字符串 // 令牌是用户程序生成的随机字符串,与微信服务器无关
$value=json_encode($cachedValue);//数组转字符串 json格式
$expire_in=config('setting.token_expire_in');//// 设置缓存失效时间
//tp5自带的cache缓存 默认文件 可以设置json sql格式
$request=cache($key,$value,$expire_in);
if (!$request){ // 令牌缓存出错
throw new TokenException([
'msg'=>'服务器缓存异常',
'errorCode'=>10005
]);
}
return $key;//返回Token
}
}
{
goCheck();//校验参数 去目录validate bebug
$ut=new UserToken($code);//service
$token=$ut->get();// bebug $token是一个字符串 建议返回json客户端
return [
'token'=>$token
];
}
}
extra
{
setting
{
'http://z.cn/images',
'token_expire_in'=>7200//配置文件中设置 cache 缓存的有效期
];
}
secure
{
'E7epHZhrTfgQ'//创建安全配置文件[盐:随机字符串]
];
}
}
Exception
{
申请令牌
申请Super令牌
支付
发货
检查登录状态
}
获取请求参数 code 并调用 PHP 接口[借助微信开发工具]
1.微信开发者工具中配置:
设置好 app_key 后,需要将 “详情” 中的 “不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书” 勾选上(在本地测试,没有远程访问的服务器或远程服务器访问的域名没有 https 证书)
2.小程序代码:
(1) 在 config 中定义 restUrl
// Protoss/utils/config.js [设置本地测试的域名基地址]
Config.restUrl = "http://mypro.com/api/v1/";
(2)在登录方法中获取 code
// 在小程序登录调用wx.login()方法中输出code,然后使用接口请求工具将code作为post请求的参数,进行调用
// Protoss/utils/token.js getTokenFromServer()
wx.login({
success: function(res) {
console.log("code: " + res.code);
}
});
3.请求 PHP 接口获取 Token
{
// 引用使用es6的module引入和定义
// 全局变量以g_开头
// 私有函数以_开头
import { Config } from "config.js";
class Token {
constructor() {
this.tokenUrl = Config.restUrl + "token/user";
}
verify() {
var token = wx.getStorageSync("token");
if (!token) {
this.getTokenFromServer();
}
}
getTokenFromServer(callBack) {
var that = this;
wx.login({
success: function(res) {
console.log("code: " + res.code);
wx.request({
url: that.tokenUrl,
method: "POST",
data: {
code: res.code
},
success: function(res) {
console.log("token: " + res.data.token);
wx.setStorageSync("token", res.data.token);
callBack && callBack(res.data.token);
}
});
}
});
}
}
export { Token };
}
【补充说明】:
(1) 需要调试时,将 XDEBUG 参数拼接到this.tokenUrl即可
(2) 如果没有输出 code, 需要关闭开发者工具后再重新启动,会自动调用该方法,并输出 code
[调用过生成的 token 已经被存储到浏览器的 Storage 中,便不会再调用 Token 请求接口,从而不产生 code]
9-9商品详情接口分析与初步编写
tp5
缓存在runtime目录下的cache
runtime
cache
{
s:104:"{"session_key":"Zlo+U9PhdvbZhf6Td4/mQA==","openid":"opOlH487rebZ-9LlQloo3NzqyZ6c","uid":"1","scope":16}";
}
商品详情
复杂
基本图片 product
很多图片
价格 库存量
product 一对多 product_image 一对一 image
商品详情 product_image
产品参数 product_property
product product_property 一对多
hasMany一对多 hasOne无外键和belongsTo有外键的一对一
belongsToMany 多对多 中间表
error
eagerlyResult 没有return
http://z.cn/api/v1/product/2 写比较完成
route
Route::get('api/:version/product/:id','api/:version.Product/getOne');
v1
{
//详细列表 有介绍价格那个 z.cn/api/v1/product/2
public function getOne($id){
(new IDMustBePostiveInt())->goCheck();
$product=ProductModel::getProductDetail($id);
if (!$product){
throw new ProductException();//内容同不覆盖 同不新exception
}
return $product;
}
}
model
Product
{
public function imgs(){
//一对多 Product - Product_imgage 商品详情
return $this->hasMany('ProductImage','product_id','id');
}
public function properties(){
//一对多 Product-product_property 商品属性
return $this->hasMany('ProductProperty','product_id','id');
}
public static function getProductDetail($id){
$product=self::with('imgs,properties')->find($id);
return $product;
}
}
ProductImage
{
belongsTo('Image','img_id','id');
}
}
}
ProductProperty
{
in App.php line 369
控制器不存在:V1
(1) 定义控制器方法 getOne($id)
(2) 定义路由 api/:version/product/:id
(3) 模型类实现[隐藏部分字段、设置数据表关联、实现数据库查询]
Product => properties => ProductProperty => 商品属性值[品名、口味、产地、保质期]
Product => imgs => Image => 商品主图
ProductImage => imgs.imgUrl => Image => 商品详情图
(4) 异常处理信息提示
[
'msg' => '当前产品无详情',
'errorCode' => 20001
]
9-10路由变量规则与分组
:id
recend 之后
访问的是:id error
Route::get('api/:version/product/:id','api/:version.Product/getOne',[],['id'=>'\d+']);
Route::get('api/:version/product/recent','api/:version.Product/getRecent');
{
//直接写 TP5的路由是顺序匹配的
//Route::get('api/:version/product/by_category','api/:version.Product/getAllInCategory');
//商品详细 对id参数进行限定 当为整数的时候才会匹配这个路由, 第三个参数为可选参数 规则是正则表达式
//Route::get('api/:version/product/:id','api/:version.Product/getOne',[],['id'=>'\d+']);
//Route::get('api/:version/product/recent','api/:version.Product/getRecent');
//路由分组闭包法TP5路由匹配的效率要高 分组执行快好性能高 1/ 2/ 要么都不加 要么一个加/ 智能行
Route::group('api/:version/product',function (){
Route::get('by_category','api/:version.Product/getAllInCategory');
Route::get(':id','api/:version.Product/getOne',[],['id'=>'\d+']);
Route::get('recent','api/:version.Product/getRecent');
});
//路由分组数组法
/*Route::group('api/:version/product',[
'by_category'=>['api/:version.Product/getAllInCategory'],
':id'=>['api/:version.Product/getOne',[],['id'=>'\d+']],
'recent'=>['api/:version.Product/getRecent']
],['method'=>'get']);*/
}
TP5的路由是顺序匹配的
这样就出错了 因为当你访问api/:version/product/recent时候匹配的是:id
解决方案 我们要对api/:version/product/:id中的id号有一个限定
//商品详细 对id参数进行限定 当为整数的时候才会匹配这个路由, 第三个参数为可选参数
Route::get('api/:version/product/:id','api/:version.Product/getOne',[],['id'=>'\d+']);
路由分组
好处
1.让你少些几个url地址
2.TP5路由匹配的效率要高
//商品详细 对id参数进行限定 当为整数的时候才会匹配这个路由, 第三个参数为可选参数 规则是正则表达式
Route::get('api/:version/product/:id','api/:version.Product/getOne',[],['id'=>'\d+']);
Route::get('api/:version/product/recent','api/:version.Product/getRecent');
//product分类详细
Route::get('api/:version/product/by_category','api/:version.Product/getAllInCategory');
1.路由匹配规则在项目中的应用。
2.存在的问题
目前调用接口都不存在问题,但是当将:id行放到recent行之前后,在调用recent路由时,则会因为优先匹配:id对应的路由,
此时则会因为参数校验不通过而报错
3.解决之道:
对路由匹配规则进行限定,设置变量规则,对于:id行,限定只有当参数为数值时才匹配到当前行。即设置 $id的变量规则
变量规则:为变量用正则的方式指定变量规则,弥补了动态变量无法限制具体的类型问题,并且支持全局规则设置。
4.代码实现[设置变量规则]
Route::get('api/:version/product/:id', 'api/:version.Product/getOne', [], ['id'=>'\d+']);
对路由配置文件中,具有相同路由前缀的路由归为同一路由组,
// 闭包方式注册路由分组
// 数组方式注册路由分组
路由分组的方式定义路由,执行的效率会比一般形式高一点。
【注】路由分组的公共路由定义时,不能在末尾加/,否则会报控制器不存在的错误
9-11闭包函数构建查询器
imgs 空
是没有
不全
全
z.cn/api/v1/product/11
z.cn/api/v1/product/2
{
{"id":11,"name":"贵妃笑 100克","price":"0.01","stock":994,"main_img_url":"http:\/\/z.cn\/images\/product-dryfruit-a@6.png","summary":null,"img_id":39,"imgs":[{"id":4,"order":1},{"id":5,"order":2},{"id":6,"order":3},{"id":7,"order":4},{"id":8,"order":5},{"id":9,"order":6},{"id":10,"order":7},{"id":11,"order":8},{"id":12,"order":9},{"id":13,"order":11},{"id":14,"order":10},{"id":18,"order":12},{"id":19,"order":13}],"properties":[{"name":"品名","detail":"杨梅","update_time":"1970-01-01 08:00:00"},{"name":"口味","detail":"青梅味 雪梨味 黄桃味 菠萝味","update_time":"1970-01-01 08:00:00"},{"name":"产地","detail":"火星","update_time":"1970-01-01 08:00:00"},{"name":"保质期","detail":"180天","update_time":"1970-01-01 08:00:00"}]}
}
id 倒序
order 不按顺序
服务器排序 完整正确
不应该客户端 也可以
闭包函数构建查询器
with([]) 数组建议
with(',') 字符串
直接order() 是当前的
先用 套用 多了 重复 了解会
public static function getProductDetail($id){
// $product=self::with('imgs.imgUrl,properties')->find($id);
$product=self::with(['imgs'=>function($query){
$query->with('imgUrl')->order('order','asc');
}])->with(['properties'])->find($id);
return $product;
}
peoduct_img表中order字段是序号 序号代表一张大图从上到下123456....
一张竖长图分隔的
在product.php 控制层中查询详细 关联了product_image表 要根据product_image的字段名升序排序
//Query
初看起来这种写法是比较麻烦的,但是你写多了这种方法是容易理解的.
学习技巧
有些东西不一定非要强制性现在搞明白
编写里面的小技巧内容写法很多.
就像刚才我们写的,不要认为多么高深.
有些时候我们不能明白某些写法,没关系.
先学会怎么用它.
比如下次遇见类似的这种情况.
对关联模型下的模型根据字段进行排序.就可以套用这种写法.
1.完成的商品详情的数据信息格式为:
2.问题:其中imgs的值为每个商品下的所有图片介绍,所以所有图片之间一定存在一定的顺序,其中imgs数组下的数据中存在order排序字段,如何对imgs的数据通过order进行排序?
3.【答】:使用闭包函数构建查询器【相当于拼接 sql】。
4.思路分析:
(1)要对 imgs 下的数据进行处理,需要获取到每组数据,然后对order字段进行排序。【通过闭包函数获取到每组数据】
(2)除了要对每组数据进行按order排序,还需要处理img_url。【通过 with 链式操作处理img_url】
5.关于闭包函数的理解:
'imgs' => function($query) {
$query->with(['imgUrl'])->order('order asc');
}
对于数组imgs,通过闭包函数,获取到每组数据,其中$query即作为参数接收每组数据的值,然后再对每组数据的img_url通过 with 进行数据关联。
9-12用户收货地址---通过令牌获取用户标识
我的
地址管理
user表 一对一 user_address
user_address
各个字段
a令牌-地址b 改变b地址
a令牌-地址b 令牌找到uid 才不改
令牌有效期 b带a令牌 令牌丢失
validate
{
'require|isNotEmpty',
'mobile'=>'require|isMobile',
'province'=>'require|isNotEmpty',
'city'=>'require|isNotEmpty',
'country'=>'require|isNotEmpty',
'detail'=>'require|isNotEmpty',
// 'uid'=>'' 不能在这传 安全 顺序 猜到 覆盖其他用户的 可令牌找到uid
];
}
}
V1
{
goCheck();
//根据Token来获取uid 不在控制写 在service 获得令牌
//根据uid来查找用户数据,判断用户是否存在,如果不存在抛出异常.
//获取用户从客户端提交来的地址信息
//根据用户地址信息是否存在,从而判断是添加地址还是更新地址
$uid=TokenService::getCurrentUid();
}
}
}
Service
{
//通用 扩展 不单令牌
public static function getCurrentTokenVar($key){
//约定 在http 头里 获得
$token=Request::instance()->header('token');
$vars=Cache::get($token);//根据key获得缓存的value
if (!$vars){
//没有抛出异常
throw new TokenException();//默认信息相同不覆盖
}else{
if (!is_array($vars)){//不是数组在转数组
$vars=json_decode($vars,true);
}
if (array_key_exists($key,$vars)){
//$vars一组 判断$key是否在$vars里 在返回对应
return $vars[$key];
}else{
//内部错误不必返回客户端 用tp自带
throw new Exception('尝试获取的Token变量并不存在');
}
}
}
public static function getCurrentUid(){
//token在缓存找到对应的uid
$uid=self::getCurrentTokenVar('uid');
return $uid;
}
}
9-12-1 通过令牌获取用户标识
(1)定义控制器方法 createOrUpdate()
(2)定义路由 api/:version/address
(3)验证器验证用户输入数据 [name, mobile, province, city, country, detail]
(4)异常处理信息提示
当数据不合法时抛出异常,而当操作成功时,也需要返回相应的数据信息。当前项目将抛出的成功信息也放在异常处理类库下。
9-12-2 面向对象的方式封装获取 uid 方法
1.通过令牌 token 即可获取缓存中对应的用户信息,而缓存中的信息包括uid scope wxResult[openid session_key]
而在 http 请求时,token 保存在 header 头信息中,获取头信息中token的方法:
$token = Request::instance()->header('token');
2.通过 json 键值对的键,获取 cache 数据
Cache::get($token)
3.增强项目的扩展性,可将通过 token 获取变量的方法进行封装。
4.代码实现:
9-14用户收货地址--模型新增和更新
hasOne 本 关联对象有外键 外键在关联对象
belongsTo 本有关联的外键 对象上无外键
User模型\表 UserAddress模型\表
V1
{
goCheck();
//根据Token来获取uid 不在控制写 在service 获得令牌
//根据uid来查找用户数据,判断用户是否存在,如果不存在抛出异常.
//获取用户从客户端提交来的地址信息
//根据用户地址信息是否存在,从而判断是添加地址还是更新地址
$uid=TokenService::getCurrentUid();
$user=UserModel::get($uid);
if (!$user){
//如果用户不存在抛异常
throw new UserException();
}
$dataArray=getDatas();//伪代码 3步 获取客户端提交来的一组数据
$userAddress=$user->address;
//模型关联的address 模型 数据库user对应的user_address表 有更新 无添加
if(!$userAddress){
// 地址不在增加
//1.user模型关联的userAddress 来增加 类似UserToken的newUser()
// $user::create(['xxx'=>$dataArray]);
//2.关联模型方法save()
$user->address()->save($dataArray);//增加address()
}
else{
$user->address->save($dataArray);//更新无() address
}
// return $user;//标准时 更新了返回
// return 'success';
return new SuccessMessage();
}
}
}
Exception
{
SuccessMessage
{
hasOne('UserAddress','user_jd','id');
}
//模型方法
public static function getByOpenID($openid){
$user=self::where('openid','=',$openid)->find();
return $user;
}
}
}
//如果在没有外键的一方调用一对一之间的关系 需要用hasOne 否则用belongsTo
//我们的目的是通过openid找到相应的用户
(4)通过模型关联,实现用户地址的新增和更新【新】
通过关联模型方法,创建数据
// 新增
$user->address()->save($dataArray);
通过关联模型属性,对当前属性对应的记录进行更新
// 更新
$user->address->save($dataArray);
(5)模型关联方法的选择:
模型关联方法的区分:
有主键关联无主键 =》 belongsTo
无主键关联有主键 =》 hasOne|hasMany
(1)user 模型定义 address()关联方法,获取到用户地址信息,当用户地址信息不存在时,也通过关联模型方法,保存地址信息
// 新增
$user->address()->save($dataArray);
(2)user 模型通过 address()关联方法关联 user_address 数据表中对应的用户地址信息,通过关联获取的数据仍然可以作为模型的属性值使用,
再通过关联模型属性,对当前属性对应的记录进行更新 [包含主键 id]
// 更新
$user->address->save($dataArray);
9-15用户收货地址--参数过滤
V1
{
$validate=new AddressNew();
$validate->goCheck();
//不能单Validata 只能验证里面有的 如果传多余的 就不能验证 危险
//比如user_id 传来 会 覆盖 危险 在baseValidata 以后其他用
$dataArray=$validate->getDataByRule(input('post.'));//伪代码 3步 获取客户端提交来的一组数据
//input('post.') 输入所有的参数
}
Validate
{
BaseValidate
{
public function getDataByRule($arrays){
if (array_key_exists('user_id',$arrays)|
array_key_exists('uid',$arrays)
){
//不应许包含user_id或者uid,防止恶意覆盖user_id外键
throw new ParameterException([
'msg'=>'参数中包含有非法的参数名user_id或者uid'
]);
}
$newArray=[];
//$this->rule 各子类 protected $rule[] key value是require过滤
foreach($this->rule as $key=>$value){
$newArray[$key]=$arrays[$key];
}
return $newArray;//只保留 $rule有的 前面检测非法请求并过滤
}
}
}
参数过滤
封装处理客户端传入的参数的方法,由于当前用户的信息是通过缓存获取的,为避免用户传入的参数造成错误修改,所以需要对客户端传入数据进行过滤,
如果携带用户 id 参数,则抛出异常,不再继续处理。除此之外,对于传入的无效、多余数据,进行过滤,仅接收验证器需要验证的字段信息。
9-16用户收货地址--接口测试
post z.cn/api/v1/address
不是code 是token
headers
token token值
body
raw json
{"name":"qiyue","mobile":"13533347986","province":"艾泽拉斯","city":"暴风城","country":"闪金镇","detail":"狮王之傲旅店"}
{"name":"koo","mobile":"13533347986","province":"艾泽拉斯","city":"暴风城","country":"闪金镇","detali":"狮王之傲旅店"}
{"name":"koo","mobile":"13533347986","province":"艾泽拉斯","city":"暴风城","country":"闪金镇","detail":"狮王之傲旅店"}
中英 逗号
单词错误
validate
basevalidate
{
protected function isMobile($value){
$rule='^1(3|4|5|7|8)[0-9]\d{8}$^';//手机号码RegExp模板
$result=preg_match($rule,$value);//RegExp
if ($result){
return true;
}else{
return false;
}
}
}
v1
// return ( new SuccessMessage(),201); 也可以
return json( new SuccessMessage(),201);
返回的状态码不预期的时候,可以自定义状态码.
//手动设置状态码
(3)对手机号的验证
正则表达式的应用场景,正则模式^1(3|4|5|6|7|8)[0-9]\d{8}$^
(6)HTTP 状态码
200:操作成功,服务器已成功处理了请求。说明:如果是对您的 robots.txt 文件显示此状态码,则表示 Googlebot 已成功检索到该文件
201:创建成功,表示服务器执行成功,并且创建了新的资源
设置接口调用成功后的状态码标识:
9-12-5 接口测试
1.需要的参数
token: header 请求头 [通过微信小程序的开发者工具]
address 字段信息 [name, mobile, province, city, country, detail]
2.返回的数据
{
"code": 201,
"msg": "ok",
"errorCode": 0
}
并且通过设置返回值为带状态码的 json 数据,json(new SuccessMessage(), 201),可将 http 的状态码也设置为201
找其他笔记 本 其他->
D:\PanDownload\【微信小程序】\微信小程序商城构建全栈应用\第10章 微信支付
10-1Scope权限作用域的应用
不是每个用户都可以删除商品
权限控制
//scope=16 代表App用户的权限数值
$cachedValue['scope']=16;//数值越大 权限也大 暂时
//scope=32 代表CMS(管理员) 用户的权限数值
// $cachedValue['scope']=32;//数值越大 权限也大 暂时
CMS 管理员
$cachedValue['scope']=16
直接写数值不易懂
枚举
php没枚举类型
模拟一个枚举
管理员大于包含用户
管理员一些不
比如Address
有令牌才可以
并且权限大于16 用户和管理员可以 特殊管理不用
tp5自带 的 前置方法
先检查权限
Service
{
////准备缓存value的数据
private function prepareCachedValue($wxResult,$uid){//缓存value wxResult,uid用户主键,scope权限e
$cachedValue=$wxResult;
$cachedValue['uid']=$uid;
//scope=16 代表App用户的权限数值
$cachedValue['scope']=ScopeEnum::User;//数值越大 权限也大 暂时
//scope=32 代表CMS(管理员) 用户的权限数值
// $cachedValue['scope']=32;ScopeEnum::Super//数值越大 权限也大 暂时
return $cachedValue;
}
}
lib\enum
{
['only'=>'second']
只有second方法 才有前置操作
先first方法 在second方法
];
{
use think\Controller;
class Address extends Controller
{
// protected $beforeActionList=[//前置操作
// 'first'=>['only'=>'second,third']//只有second third方法 才有前置操作先first方法 在second方法
// ];
// protected function first(){echo 'first';}
// //API接口
// public function second(){echo 'second';}//z.cn/api/v1/address/second
// public function third(){echo 'third';}//z.cn/api/v1/address/third 配ruote 不就全url
}
}
//Route::get('api/:version/second','api/:version.Address/second');
//Route::get('api/:version/third','api/:version.Address/third');
前置操作
在我们编写 api 业务逻辑的时候,我们会想在调用 api 接口之前,需要满足某些条件
这样才可以去访问我们的接口中的业务逻辑
所以我们要在做一个前置操作,抵挡不满足条件的抛出异常
tp5 中使用前置操作需要基础自带的一个基类 Controller
定义一个名为 $beforeActionList 的数组
{
use think\Controller
class Address extends Controller
{
// 定义前置属性
// 第一个字段是 访问api接口前 需要 访问的一个前置方法
// 箭指的 是一个数组
// 数组内部定义一个箭指数据,也可以直接是一个字符串(内部填入api接口函数就可以了)
// 否则向下面这样写
// 多api编写
protected $beforeActionList = [
'first' => ['only' => 'second,third']
];
// 触发api前 执行的前置函数
protected function first () {
echo 'first';
}
// api接口
public function second () {
echo 'second';
}
// api接口
public function third () {
echo 'third';
}
}
}
10-3对Aress接口做权限控制
关于ThinkPHP5前置操作不生效的问题
https://blog.csdn.net/qq_38377299/article/details/78023836
将$beforeActionList属性中的
'DelCateSon' => ['only'=>'Del'],
改为
'DelCateSon' => ['only'=>'del'],
限定条件的方法名改为小写 (即使你实际的方法名是大写开头)
才可以重新生效
前面可以大写 only后面小写
旧版要小写 新版不区分配套
提到基类
拦截器
tp5不够好
其他语言java @check... 某[]
Exception
{
['only'=>'createorupdateaddress']//要小写createOrUpdateAddress
];
protected function checkPrimaryScope(){
$scope=TokenService::getCurrentTokenVar('scope');
if ($scope){//是否存在
if ($scope>=ScopeEnum::User){//权限大于16
return true;
}else{
throw new ForbiddenException();//抛出异常中断后面操作
}
}
else{
throw new TokenException();//Token已过期或无效Token
}
}
}
10-4下单与支付的业务流程(库存量检测)
下单接口
到时提 现不
实时同步 各种技术 维护难 成本高 不 ...
客户端和服务器数据库等不同步 其他用户买走
支付 在检测 可以等待一会
1.用户在选择商品后,向AP1提交包含它所选择商品的相关信息
2.APT在接收到信息后,需要检查订单相关商品的库存量
3.有库存,把订单数据存入数据库中:下单成功了,返回客户端消息,告诉客户端可以支付了
4.调用我们的支付接口,进行支付
5.还需要再次进行库存量检测
6.服务器这边就可以调用微信的支付接口进行支付
7.微信会返回给我们一个支付的结果(异步)
8.成功:也需要进行库存量的检查
9.成功:进行库存量的扣除,[微信异步 失败:返回一个支付失败的结果]
//下单接口 下单数据增加记录 复杂 库存量 支付
V1
class Order extends Controller
10-5下单与支付详细流程
流程图
https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=7_4&index=2
微信 异步 多次发送 到服务器响应才 ..
10-6重构权限控制前置方法
管理员只能添加删除 修改专题..
支付不应该
不建议改源码 升级等问题
v1
order
{
['only'=>'placeorder']//要小写createOrUpdateAddress
];
// protected function checkExclusiveScope(){
// //管理员只能添加删除 修改专题..
// //支付不应该 排除管理员
// TokenService::needExclusiveScope();
//// $scope=TokenService::getCurrentTokenVar('scope');
//// if ($scope){//是否存在
//// if ($scope==ScopeEnum::User){//权限==16 是用户才能
//// return true;
//// }else{
//// throw new ForbiddenException();//抛出异常中断后面操作
//// }
//// }
//// else{
//// throw new TokenException();//Token已过期或无效Token
//// }
// }
public function placeOrder(){
}
// public function deleteOrder(){
// 多方法 很多前置 oop 封装
// }
}
}
Address
{
//保留前置操作 还不够好 其他接口不断重复 多代码
protected $beforeActionList=[//前置操作
'checkPrimaryScope'=>['only'=>'createorupdateaddress']//要小写createOrUpdateAddress
];
//前保留 后可以提baseController
// protected function checkPrimaryScope(){
//重构方法 oop 封装 提到 Service Token
// TokenService::needPrimaryScope();
// $scope=TokenService::getCurrentTokenVar('scope');
// if ($scope){//是否存在
// if ($scope>=ScopeEnum::User){//权限大于16
// return true;
// }else{
// throw new ForbiddenException();//抛出异常中断后面操作
// }
// }
// else{
// throw new TokenException();//Token已过期或无效Token
// }
// }
}
BaseController
{
=ScopeEnum::User){//权限>=16
return true;
}else{
throw new ForbiddenException();//抛出异常中断后面操作
}
}
else{
throw new TokenException();//Token已过期或无效Token
}
}
//只有用户才能访问的接口权限
public static function needExclusiveScope(){
$scope=self::getCurrentTokenVar('scope');
if ($scope){//是否存在
if ($scope==ScopeEnum::User){//权限=16
return true;
}else{
throw new ForbiddenException();//抛出异常中断后面操作
}
}
else{
throw new TokenException();//Token已过期或无效Token
}
}
}
Route
Route::post('api/:version/order','api/:version.Order/placeOrder');
重构前置验证操作 (实现面向对象)
提取验证业务逻辑到 service 的基类中
提取前置方法到 BaseController 的基类中
继承基类,执行前置方法
提取出一个前置的基类 BaseController (继承内置 Controller)
// 前置方法
// 验证初级权限作用域,用户和cms都可以访问
// 验证权限,只有用户可以访问,cms无法访问
// 向Token调用验证方法
提取验证业务逻辑(因为是 token 相关的就归并到 token 的 service 业务层中)
// 调用token中的方法获取scope
继承 BaseController 基类使用前置方法
10-7编写一个复杂的验证器
多个商品id 数量
数组验证 难
格式化代码:ctrl=alt=L
V1
{
public function placeOrder(){
(new OrderPlace())->goCheck();
}
}
validate
{
1,
// 'count' => 3
// ], [
// 'product_id' => 2,
// 'count' => 3
// ], [
// 'product_id' => 11,
// 'count' => 3
// ]
// ];
protected $rule=[
'products'=>'checkProducts'
];
//只有$rule自动执行 singleRule手动
protected $singleRule=[
'product_id'=>'require|isPostiveInt',
'count'=>'require|isPostiveInt'
];
//不同其他 没其他调用 写在子类里
protected function checkProducts($values){
//判断最层是否 空 | 数组
if (!is_array($values)){
throw new ParameterException([
'msg'=>'商品参数不正确'
]);
}
if(empty($values)){
throw new ParameterException([
'msg'=>'商品列表不能为空'
]);
}
//二维数组 里面的数组
foreach($values as $value){
$this->checkProduct($value);
}
return true;//都通过true
}
protected function checkProduct($value){
//只有$rule自动执行 singleRule手动
$validate=new BaseValidate($this->singleRule);
$result=$validate->check($value);
if (!$result){
//验证不通过
throw new ParameterException([
'msg'=>'商品列表参数错误'
]);
}
}
}
}
验证器数据自定义子项验证
自定义子项验证,通过自定义的方法调用实现
当我们在验证时,传入的是一个二维数组,就可以使用来验证子项
我们就自定义一个验证的方法,通过基类的验证的调用
// 整体验证
// 数据子项的验证
// 循环对每一项进行验证
// 基础调用子项验证
10-8下单接口业务模型一
v1
{
public function placeOrder(){
(new OrderPlace())->goCheck();
$products=input('post.products/a');//获取传递过来数组 还要加a才可以
$uid=TokenService::getCurrentUid();
//业务复杂 写Service
}
}
service
{
oProducts=$oProducts;
$this->products=$this->getProductsByOrder($oProducts);//数据库查询出来
$this->uid=$uid;
}
//根据订单信息查找真实的商品信息
private function getProductsByOrder($oProducts){
// foreach ($oProducts as $oProduct){
// //循环的查询数据库 把 数据库查崩溃 多 大 占资源
// }
//循环提交来的商品 放到一个数组$oPIDs里 在对比数据库 数量可控
$oPIDs=[];
foreach ($oProducts as $item) {
array_push($oPIDs,$item['product_id']);
}
$products=Product::all($oPIDs)//对应的商品
->visible(['id','price','stock','name','main_img_url'])//只显示
->toArray();//转数组 不select
return $products;
}
}
}
10-9下单接口业务模型二
一个订单不通过 都不通过
分销 多用户 微信-
多复杂的逻辑 多封装方法 好看
10行
50
Service
Order
{
oProducts=$oProducts;
$this->products=$this->getProductsByOrder($oProducts);//数据库查询出来
$this->uid=$uid;
}
//提交商品和库存量对比
private function getOrderStatus(){
$status=[
'pass'=>true,//检测通过
'orderPrice'=>0,//所有商品总价
'pStatusArray'=>[]//商品
];
foreach ($this->oProducts as $oProduct){
// $oPID=$oProduct['id'];
// $oCount=$oProduct['count'];
$pStatus=$this->getProductStatus($oProduct['product_id'],$oProduct['count'],$this->products);
if (!$pStatus['haveStock']){
$status['pass']=false;//又一个订单不通过 false
}
$status['orderPrice']+=$pStatus['totalPrice'];//所有商品总价
array_push($status['pStatusArray'],$pStatus);//商品全部信息
}
return $status;
}
private function getProductStatus($oPID,$oCount,$products){
//把提交的商品id 数量 对应的库存量
$pIndex=-1;
//$pStatus某个商品 历史订单
$pStatus=[
'id'=>null,//商品id
'haveStock'=>false,//库存量
'count'=>0,//提交的数目
'name'=>'',//商品名
'totalPrice'=>0 //单个商品 多少个 总价
];
//小于 sql库存的总和 判断商品存在
for ($i=0;$i'id为:'.$oPID.'的商品不存在,创建订单失败'
]);
}
else{
$product=$products[$pIndex];//对应商品
$pStatus['id']=$product['id'];
$pStatus['name']=$product['name'];//每次调用传一个商品 对应name
$pStatus['count']=$oCount;
$pStatus['totalPrice']=$product['price']*$oCount;
if ($product['stock']-$oCount>=0){//是否有库存量
$pStatus['haveStock']=true;
}
}
return $pStatus;
}
//根据订单信息查找真实的商品信息
private function getProductsByOrder($oProducts){
// foreach ($oProducts as $oProduct){
// //循环的查询数据库 把 数据库查崩溃 多 大 占资源
// }
//循环提交来的商品 放到一个数组$oPIDs里 在对比数据库 数量可控
$oPIDs=[];
foreach ($oProducts as $item) {
array_push($oPIDs,$item['product_id']);
}
$products=Product::all($oPIDs)//对应的商品
->visible(['id','price','stock','name','main_img_url'])//只显示
->toArray();//转数组 不select
return $products;
}
}
}
exception
{
oProducts=$oProducts;
$this->products=$this->getProductsByOrder($oProducts);//数据库查询出来
$this->uid=$uid;
$status=$this->getOrderStatus();
if(!$status['pass']){
//库存量不通过 返回 一个标记
$status['order_id']=-1;//新增一个标记
return $status;
}
//开始创建订单
}
}
10-11订单快照的实现
概况
一个 3个等...
一共多少商品
超过1个 加个等
Service
Order
{
//开始创建订单
$orderSnap=$this->snapOrder();
//生产订单快照
private function snapOrder($status){
$snap=[
'orderPrice'=>0,//总价格
'totalCount'=>0,//所有总数
'pStatus'=>[],//状态
'snapAddress'=>null,//地址
'snapName'=>'',//商品概况
'snapImg'=>'' //商品图片
];
$snap['orderPrice']=$status['orderPrice'];
$snap['totalCount']=$status['totalCount'];
$snap['pStatus']=$status['pStatusArray'];
$snap['snapAddress']=json_encode($this->getUserAddress());//存数组到数据库不太好,可以 转json对象 否则创建一个表 或用文档数据库
$snap['snapName']=$this->products[0]['name'];//第一个为概况
$snap['snapImg']=$this->products[0]['main_img_url'];
if (count($this->products)>1){
$snap['snapName'].='等';//+= 追加 超过1个 加个等
}
}
private function getUserAddress(){
//有地址才可以下单成功 无exception user表模型 和 user_address 外键user_id 一起
$userAddress=UserAddress::where('user_id','=',$this->uid)->find();
if (!$userAddress){
throw new UserException([
'msg'=>'用户收货地址不存在,下单失败',
'errorCode'=>60001,
]);
}
return $userAddress->toArray();//查找出来是query对象 返回数组
}
}
10-12订单创建
订单编号 唯一的
高并发 有小概率 重复
加大位数 避免重复
模型类 和 本类 不能 重名 as别名 否则全路径命名空间
order 有snap
order talat_count 所有商品总和
多对多
order_product count 某个商品自己的总和 特殊的中间表
order_id(等order表id) product_id(提交上来的) count(提交上)
多对多
product
foreach ($this->oProducts as &$p){//php语法 &才能对数组修改
$p['order_id']=$orderID;
}
中间表不用隐藏字段
$orderProduct->order_id=$orderID;
$orderProduct->save(); save保存一个一个字段
$orderProduct->saveall([]); saveall 一组数据
下单时间
Service
Order
{
//真正创建订单 存入数据库
private function createOrder($snap){
try {//数据库操作最好 加个 try异常处理
$orderNo = $this->makeOrderNo();//订单号
$order = new \app\api\model\Order();
$order->user_id = $this->uid;//数据表字段 对应的
$order->order_no = $orderNo;
$order->total_price = $snap['orderPrice'];
$order->total_count = $snap['totalCount'];
$order->snap_img = $snap['snapImg'];
$order->snap_name = $snap['snapName'];
$order->snap_address = $snap['snapAddress'];
$order->snap_items = json_encode($snap['pStatus']);//存数组到数据库不太好,可以 转json对象 否则创建一个表 或用文档数据库
$order->save();//写入数据库
$orderID = $order->id;//保存了 获取order id
$create_time = $order->create_time;//保存了 获取创建数据 tp自带
foreach ($this->oProducts as &$p) {//php语法 &才能对数组修改 可能改原数组 $p$oProduct指向同一个地址
$p['order_id'] = $orderID;
}
$orderProduct = new OrderProduct();
$orderProduct->saveAll($this->oProducts);//saveall 一组数据
return [
'order_no' => $orderNo,//订单号
'order_id' => $orderID,//order id 下单id
'create_time' => $create_time //下单时间
];
}
catch (Exception $ex){
//通用异常
throw $ex;
}
// $orderProduct->order_id=$orderID;
// $orderProduct->product_id=$this->oProducts['product_id'];
// $orderProduct->count=$this->oProducts['count'];
// $orderProduct->save();
}
public static function makeOrderNo()
{//其他也可以调用 公共 订单编号 唯一的 订单方法其一 ..
$yCode = array('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J');
$orderSn =
$yCode[intval(date('Y')) - 2017] . strtoupper(dechex(date('m'))) . date(
'd') . substr(time(), -5) . substr(microtime(), 2, 5) . sprintf(
'%02d', rand(0, 99));
return $orderSn;
}
}
Model
{
goCheck();
$products=input('post.products/a');//获取传递过来数组 还要加a才可以
$uid=TokenService::getCurrentUid();
//业务复杂 写Service
$order=new OrderService();
$status=$order->place($uid,$products);
return $status;//返回客户端 下单编号 id 时间 通过
}
}
service
order
{
public function place($uid,$oProducts){
//oProducts和products 作对比
//products从数据库中查询出来
$this->oProducts=$oProducts;
$this->products=$this->getProductsByOrder($oProducts);//数据库查询出来
$this->uid=$uid;
$status=$this->getOrderStatus();
if(!$status['pass']){
//库存量不通过 返回 一个标记
$status['order_id']=-1;//新增一个标记
return $status;
}
//开始创建订单 通过的时候
$orderSnap=$this->snapOrder($status);
$order=$this->createOrder($orderSnap);
$order['pass']=true;//追加订单通过
return $order;
}
}
10-14测试订单接口
看主方法 大概 再细节 看源码方法
小程序 先 申请令牌 缓存有令牌 再支付 那有发货的订单之类
小程序基础框架 扩展
tp 简单 create_time 问题
10-15自动写入时间戳
手动可以 很多要用多改 麻烦
框架自带好
模型
protected $autoWriteTimestamp=true;//create_time
// protected $createTime="create_timestamp";//自定义create_time名
delete_time 有值就是删除 假删除
model
order
{
makeOrderNo();
$order = new \app\api\model\Order();
$order->user_id = $this->uid;
$order->order_no = $orderNo;
$order->total_price = $snap['orderPrice'];
$order->total_count = $snap['totalCount'];
$order->snap_img = $snap['snapImg'];
$order->snap_name = $snap['snapName'];
$order->snap_address = $snap['snapAddress'];
$order->snap_items = json_encode($snap['pStatus']);
$order->save();
$orderID = $order->id;
$create_time = $order->create_time;
foreach ($this->oProducts as &$p) {
$p['order_id'] = $orderID;
}
$orderProduct = new OrderProduct();
$orderProduct->saveAll($this->oProducts);
// 结尾加上结束
Db::commit();
return [
'order_no' => $orderNo,
'order_id' => $orderID,
'create_time' => $create_time
];
} catch (Exception $ex) {
// 异常出现回滚
Db::rollback();
throw $ex;
}
}
10-17关于微信支付
个人不能支付
先学 用在重新看|下 del过
申请企业账户 密码
V1
Pay
自己屏蔽 流程过一下
10-18支付的服务器端编写一
先生成预订单 到时 微信服务器订单
也要进行前置处理 extend baseController
需要什么参数 看微信支付需要什么参数
https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=9_1&index=1
找需要 找必填
自带小程序id...
tokenoppid 订单号id orderNo其一
支付复杂 加Service
V1
{
['only'=>'getpreorder']//要小写createOrUpdateAddress
];
//先生成预订单 到时 微信服务器订单
public function getPreOrder($id=''){
(new IDMustBePostiveInt())->goCheck();
}
}
}
Service
{
orderID=$orderID;
}
//主方法
public function pay(){
//再次检测库存量 用户订单但久没才支付 别人买走没货...
}
}
}
支付 //再次检测库存量 用户订单但久没才支付 别人买走没货...
route
Route::post('api/:version/pay/pre_order','api/:version.Pay/getPreOrder');
10-19支付的服务器端编写二
代码合理拆分
复用
getOrderStatus库存量检测 需要 oProduct Product
$orderID 模型|表order_id 得 oProduct
getProductsByOrder 需要oProduct 得 product
service
Order
{
public function checkOrderStock($orderID){
$oProducts=OrderProduct::where('order_id','=',$orderID)->select();
$this->oProducts=$oProducts;
$this->products=$this->getProductsByOrder($oProducts);
$status=$this->getOrderStatus();
return $status;
}
}
10-20支付的服务端编写三
不分先后
最后几率大的 危险的先 数据库先 后面不再执行
订单号可能根本就不存在
订单号确实是存在的,但是,订单号和当前用户是不匹配的
可能其他用封装到ServiceToken
订单有可能已经被支付过
进行库存量检测
数据库表 order status表示被支付的状态 有注释
1.未支付 2.已支付 3.已发货 4.已支付,但库存量不足 没
不注释//
用枚举 其他用 容易看
Service
Pay
{
orderID=$orderID;
}
//主方法
public function pay(){
//再次检测库存量 用户订单但久没才支付 别人买走没货...
//1.订单号可能根本就不存在
//订单号确实是存在的,但是,订单号和当前用户是不匹配的 可能其他用封装到ServiceToken
//订单有可能已经被支付过
//进行库存量检测最后
$this->checkOrderValid();
$orderService=new OrderService();
$status=$orderService->checkOrderStock($this->orderID);//进行库存量检测
if (!$status['pass']){
return $status;//告诉客户端订单不通过
}
}
private function makeWxPreOrder(){
//微信预订单
}
private function checkOrderValid(){
//检测订单是否存在
$order=OrderModel::where('id','=',$this->orderID)->find();
if (!$order){
throw new OrderException();
}
if (!Token::isValidOperate($order->user_id)){
//订单号和当前用户是不匹配的 $order模型下有uid
throw new TokenException([
'msg'=>'订单与用户不匹配',
'errorCode'=>10003
]);
}
if ($order->status!=OrderStatusEnum::UNPAID){//订单有可能已经被支付过
//数据库表 order status表示被支付的状态 有注释
// 1.未支付 2.已支付 3.已发货 4.已支付,但库存量不足
//用枚举 其他用 容易看
throw new OrderException([
'msg'=>'订单已支付过了',//订单异常
'errorCode'=>80003,
'code'=>400
]);
}
$this->orderNO=$order->order_no;//订单号
return true;
}
}
}
Token
{
//检测当前操作用户
public static function isValidOperate($checkedUID){
if (!$checkedUID){
throw new Exception('检查UID时必须传入一个被检查的UID');
}
$currentOperateUID=self::getCurrentUid();
if ($currentOperateUID==$checkedUID){
return true;
}
return false;
}
}
enum
{
WxPayUnifiedOrder
PSR-4,PSR-0 不用 require 8.php
用命名空间
use调用
而微信sdk没有命名空间
api接口是:WxPay.Api.php
方法中一个:
//手动加载loader 目录.文件前面第一个 extend跟目录 文件后面到后缀
//extend/ WxPay/WxPay .Api.php
Loader::import('WxPay.WxPay',EXTEND_PATH,'.Api.php');
不为什么 规则作者 用
还有许多其他方法:
由于TP5.1取消了Loader载入的方法,所以我找了好久,使用的是Env来获取微信文件的地址。通过此来加载SDK的文件。
错 5.1->还有
include Env::get('root_path') . 'extend/wxPay/WxPay.Api.php';
extend目录 有命令空间的 自动加载 没loader等
不改变量 EXTEND_PATH
没命名空间 要加 \
自动加载
https://www.kancloud.cn/manual/thinkphp5/118015
如果你不需要系统的自动加载功能,又或者没有使用命名空间的话,那么也可以使用think\Loader类的import方法手动加载类库文件,
Service
Pay
{
use think\Loader;
//手动加载loader 目录.文件前面第一个 extend跟目录 文件后面到后缀
//extend/ WxPay/WxPay .Api.php
Loader::import('WxPay.WxPay',EXTEND_PATH,'.Api.php');
private function makeWxPreOrder(){
//微信预订单
//openid 不是自己sql的UID
$openid=Token::getCurrentTokenVar('openid');
if (!$openid){
throw new TokenException();
}
//wx-sdk 统一下单类 WxPay.Data.php ->WxPayUnifiedOrder
$WxOrderData=new \WxPayUnifiedOrder();//没命名空间 要加 \
}
}
引入没有命名空间的文件与调用(Loader),手动引入微信支付 php
使用 loader 的 import 方法
extend/WxPay/WePay.Api.php
{
// 文件开头的第一个 文件路径 // 类的名称
Loader::import('WxPay.WxPay',EXTEND_PATH,'.Api.php');
// 调用
// 调用的时候前面要加反斜杠
$wxOrderData = new \WxPayUnifiedOrder();
}
3.根据订单号实现预支付
根据订单号来判断是否存在该订单号,该订单号是否属于该token的uid,该订单号的状态是否为未支付,最后还需要再检查库存。
注:部分和上面方法通用,需写为一个通用的方法。
实现了上面的步骤之后,就可以使用起微信的SDK了。
将微信的文件放在了该目录下:
10-22支付的服务器端编写五
使用api需要配置一些文件 参数
WxPay.Config.php
{
* APPID:绑定支付的APPID(必须配置,开户邮件中可查看)
*
* MCHID:商户号(必须配置,开户邮件中可查看)
*
* KEY:商户支付密钥,参考开户邮件设置(必须配置,登录商户平台自行设置)
* 设置地址:https://pay.weixin.qq.com/index.php/account/api_cert
*
* APPSECRET:公众帐号secert(仅JSAPI支付的时候需要配置, 登录公众平台,进入开发者中心可设置),
* 获取地址:https://mp.weixin.qq.com/advanced/advanced?action=dev&t=advanced/dev&token=2005451881&lang=zh_CN
* @var string
*/
const APPID = 'your appid';
const MCHID = 'your mech id';
const KEY = 'your mech key';
const APPSECRET = 'your appsecret';
}
MCHID:商户号(必须配置,开户邮件中可查看)
*
KEY:商户支付密钥,参考开户邮件设置(必须配置,登录商户平台自行设置)
企业注册才有
没有 不用 维持
过
service
Pay
{
private function makeWxPreOrder($totalPrice){
//微信预订单
//openid 不是自己sql的UID
$openid=Token::getCurrentTokenVar('openid');
if (!$openid){
throw new TokenException();
}
//wx-sdk 统一下单类 WxPay.Data.php ->WxPayUnifiedOrder
$wxOrderData=new \WxPayUnifiedOrder();//没命名空间 要加 \
$wxOrderData->SetOut_trade_no($this->orderNO);//订单号
$wxOrderData->SetTrade_type('JSAPI');//交易类型
$wxOrderData->SetTotal_fee($totalPrice*100);//总金额分为单位 $status里有元为单位
$wxOrderData->SetBody('零食商贩');//商品简单描述
$wxOrderData->SetOpenid($openid);//用户标识
$wxOrderData->SetNotify_url('');//微信回调url 异步接收 通知
}
private function getPaySignature($wxOrderData){
//签名参数结果
//这调用微信预订单接口 请求
$wxOrder=\WxPayApi::unifiedOrder($wxOrderData);
if ($wxOrder['return_code']!='SUCCESS'||
$wxOrder['result_code']!='SUCCESS'){
Log::record($wxOrder,'error');//失败 记录日志
Log::record('获取预支付订单失败','error');
}
}
}
新版
注:微信支付的WxPay.config改变了,
我们需要继承该方法,填入自己的appid,appsecret和mmid等信息才行。
引入微信的SDK后,首先需要做的就是设置各种属性。
{
$wxOrderData = new \WxPayUnifiedOrder();
// 会通过异步传回来给我们
$wxOrderData->SetOut_trade_no($this->orderNum);
$wxOrderData->SetTrade_type('JSAPI');
$wxOrderData->SetTotal_fee($totalPrice * 100);
$wxOrderData->SetBody('零食商贩');
$wxOrderData->SetOpenid($openid);
// 支付成功后,微信的回调,要用post提交
$wxOrderData->SetNotify_url('http://paysdk.weixin.qq.com/notify.php');
return $this->getPaySignature($wxOrderData);
}
设置好了微信需要的信息之后,就开始发送到微信了,这里需要将继承的config传入里面发送给微信
{
private function getPaySignature($wxOrderData)
{
$config = new WxPayConfig();
$wxOrder = \WxPayApi::unifiedOrder($config, $wxOrderData);
if ($wxOrder['return_code'] != 'SUCCESS' || $wxOrder['result_code'] != 'SUCCESS') {
Log::record($wxOrder, 'error');
Log::record('获取支付订单失败', 'error');
}
// 记录prepay_id
$this->recordPayOrder($wxOrder);
$signature = $this->sign();
return $signature;
}
}
10-23支付的服务器端编写六
微信api WxPay.Api.php
没有证书改 false 严格模式
curl_setopt($ch,CURLOPT_SSL_VERIFYHOST,FALSE);//严格校验
prepay_id 成功 可以发货...
到时 微信返回结果 进一步处理
return_code="FAil"
return_msg="mch_id参数格式错误"
微信支付api 没有企业 随便打
俩个自己
const APPID = 'wx3ce92e92af9b29b1';
const MCHID = '10000100';
const KEY = '192006250b4c09247ec02edce69f6a2d';
const APPSECRET = '4ceea0beecb10a88e79368e7df9a12dc';
还有是不行 不匹配
自接口对 视频更对
没企业 对过
->
返回结果 参考 文档 视频|截图
https://pay.weixin.qq.com/wiki/doc/api/external/jsapi.php?chapter=9_7
$wxOrder=\WxPayApi::unifiedOrder($wxOrderData);//debug 看返回结果
大概
appid
mch_id
nonce_str
prepay_id
result_code
return_code
sign
trade_type
...视频|截图 文档 ->
V1
{
['only'=>'getpreorder']//要小写createOrUpdateAddress
];
//先生成预订单 到时 微信服务器订单
public function getPreOrder($id=''){
(new IDMustBePostiveInt())->goCheck();
$pay=new PayService($id);
$pay->pay();
}
}
}
Service
Pay
{
orderID=$orderID;
}
//主方法
public function pay(){
//再次检测库存量 用户订单但久没才支付 别人买走没货...
//1.订单号可能根本就不存在
//订单号确实是存在的,但是,订单号和当前用户是不匹配的 可能其他用封装到ServiceToken
//订单有可能已经被支付过
//进行库存量检测最后
$this->checkOrderValid();
$orderService=new OrderService();
$status=$orderService->checkOrderStock($this->orderID);//进行库存量检测
if (!$status['pass']){
return $status;//告诉客户端订单不通过
}
return $this->makeWxPreOrder($status['orderPrice']);
}
private function makeWxPreOrder($totalPrice){
//微信预订单
//openid 不是自己sql的UID
$openid=Token::getCurrentTokenVar('openid');
if (!$openid){
throw new TokenException();
}
//wx-sdk 统一下单类 WxPay.Data.php ->WxPayUnifiedOrder
$wxOrderData=new \WxPayUnifiedOrder();//没命名空间 要加 \
$wxOrderData->SetOut_trade_no($this->orderNO);//订单号
$wxOrderData->SetTrade_type('JSAPI');//交易类型
$wxOrderData->SetTotal_fee($totalPrice*100);//总金额分为单位 $status里有元为单位
$wxOrderData->SetBody('零食商贩');//商品简单描述
$wxOrderData->SetOpenid($openid);//用户标识
$wxOrderData->SetNotify_url('http://qq.com');//微信回调url 异步接收 通知 测试随便个
return $this->getPaySignature($wxOrderData);
}
private function getPaySignature($wxOrderData){
//签名参数结果
//这调用微信预订单接口 请求
$wxOrder=\WxPayApi::unifiedOrder($wxOrderData);//debug 看返回结果
if ($wxOrder['return_code']!='SUCCESS'||
$wxOrder['result_code']!='SUCCESS'){
Log::record($wxOrder,'error');//失败 记录日志
Log::record('获取预支付订单失败','error');
}
return null;
}
private function checkOrderValid(){
//检测订单是否存在
$order=OrderModel::where('id','=',$this->orderID)->find();
if (!$order){
throw new OrderException();
}
if (!Token::isValidOperate($order->user_id)){
//订单号和当前用户是不匹配的 $order模型下有uid
throw new TokenException([
'msg'=>'订单与用户不匹配',
'errorCode'=>10003
]);
}
if ($order->status!=OrderStatusEnum::UNPAID){//订单有可能已经被支付过
//数据库表 order status表示被支付的状态 有注释
// 1.未支付 2.已支付 3.已发货 4.已支付,但库存量不足
//用枚举 其他用 容易看
throw new OrderException([
'msg'=>'订单已支付过了',//订单异常
'errorCode'=>80003,
'code'=>400
]);
}
$this->orderNO=$order->order_no;//订单号
return true;
}
}
}
数据库锁与事务锁的区别
数据库模型->lock(true)
事务锁 Db
事务锁是等待整个事务提交才会执行第二次事务,但是数据库模型锁只是单步的锁着了数据库查询语句
在后面的操作还没有执行时,数据库模型锁已经放开了
10-24支付的故武器端编写七
要返回客户端 一级一级return 返回
return $wxOrder;
控制层 也return
prepay_id 预支付id模板 保存
保存到order数据表 prepay_id字段
record 主要 primary
second 次要
Service
Pay
{
private function getPaySignature($wxOrderData){
//签名参数结果
//这调用微信预订单接口 请求
$wxOrder=\WxPayApi::unifiedOrder($wxOrderData);//debug 看返回结果
if ($wxOrder['return_code']!='SUCCESS'||
$wxOrder['result_code']!='SUCCESS'){
Log::record($wxOrder,'error');//失败 记录日志
Log::record('获取预支付订单失败','error');
}
// return $wxOrder;要返回客户端 一级一级return 返回 return $wxOrder;控制层 也return
//prepay_id 保存到order数据表 prepay_id字段
$this->recordPreOrder($wxOrder);
return null;
}
private function recordPreOrder($wxOrder){
//prepay_id 预支付id模板 保存
// 保存到order数据表 prepay_id字段
//已经有订单号 是更新
OrderModel::where('id','=',$this->orderID)
->update(['prepay_id'=>$wxOrder['prepay_id']]);
}
}
微信返回一些字段 和 diy新增的一些 返回客户端
10-25支付的小程序端讲解(含标签的作用讲解)八
服务端 拉起 微信api
$wxOrder=\WxPayApi::unifiedOrder($wxOrderData);//debug 看返回结果
客户端 也有 拉起 api
wx.requestPayment(object)
参数 依靠服务端
{
属性 类型 默认值 必填 说明
timeStamp string 是 时间戳,从 1970 年 1 月 1 日 00:00:00 至今的秒数,即当前的时间
nonceStr string 是 随机字符串,长度为32个字符以下
package string 是 统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=***
signType string MD5 否 签名算法
paySign string 是 签名,具体签名方案参见 小程序支付接口文档
success function 否 接口调用成功的回调函数
fail function 否 接口调用失败的回调函数
complete function 否 接口调用结束的回调函数(调用成功、失败都会执行)
}
https://mp.weixin.qq.com/debug/wxadoc/dev/api/api-pay.html
wx.requestPayment({
timeStamp: '',当前的时间
nonceStr: '',随机字符串
package: '',prepay_id=*** 数据表里那个
signType: 'MD5',签名算法
paySign: '',签名 加密方式 防止篡改
success (res) { },
fail (res) { }
})
paySign 签名参数 加密方式 防止篡改 支付宝阿里云...
签名 算法 步骤
服务端 ParamAParamBParamC APPKey
ParamAParamBParamC v| Sign签名
客户端 ParamAParamBParamC => 微信 APPKey 比较客户端传递来签名和微信计算的签名做对比
相对 没绝对 防止篡改 服务端到客户端
如果获取到的信息正确,
就需要将需要的信息返回给客户端,客户端根据此信息来调用微信的支付接口。
根据微信支付的信息
,返回给客户端相应的信息。SDK在设置完之后,通过GetValue可以传换成数组
10-26支付的服务器端编写九
V1
pay
{
public function getPreOrder($id=''){
(new IDMustBePostiveInt())->goCheck();
$pay=new PayService($id);
return $pay->pay();//返回客户端 测试
}
}
Service
pay
{
private function getPaySignature($wxOrderData){
//签名参数结果
//这调用微信预订单接口 请求
$wxOrder=\WxPayApi::unifiedOrder($wxOrderData);//debug 看返回结果
if ($wxOrder['return_code']!='SUCCESS'||
$wxOrder['result_code']!='SUCCESS'){
Log::record($wxOrder,'error');//失败 记录日志
Log::record('获取预支付订单失败','error');
}
// return $wxOrder;要返回客户端 一级一级return 返回 return $wxOrder;控制层 也return
//prepay_id 保存到order数据表 prepay_id字段
$this->recordPreOrder($wxOrder);
$signature=$this->sign($wxOrder);//返回客户端需要参数wx.requestPayment
return $signature;
}
private function sign($wxOrder){
//手动算可以 但wxapi提供 signType
$jsApiPayData=new \WxPayJsApiPay();
$jsApiPayData->SetAppid(config('wx.app_id'));
$jsApiPayData->SetTimeStamp((string)time());//接收string 时间戳
$rand=md5(time(),mt_rand(0,1000));//随机字符串
$jsApiPayData->SetNonceStr($rand);
$jsApiPayData->SetPackage('prepay_id='.$wxOrder['prepay_id']);//格式 a=a
$jsApiPayData->SetSignType('md5');//加密方式
$sign=$jsApiPayData->MakeSign();//自带签名算法 防止篡改 这调用好学习这api方式
$rawValues=$jsApiPayData->GetValues();//转换成数组 返回客户端
$rawValues['paySign']=$sign;//客户端需要 自加
unset($rawValues['appId']);//客户端不需要 删除
return $rawValues;
}
private function recordPreOrder($wxOrder){
//prepay_id 预支付id模板 保存
// 保存到order数据表 prepay_id字段
//已经有订单号 是更新
OrderModel::where('id','=',$this->orderID)
->update(['prepay_id'=>$wxOrder['prepay_id']]);
}
}
设置好了微信需要的信息之后,就开始发送到微信了,这里需要将继承的config传入里面发送给微信
如果获取到的信息正确,就需要将需要的信息返回给客户端,客户端根据此信息来调用微信的支付接口。
根据微信支付的信息,返回给客户端相应的信息。SDK在设置完之后,通过GetValue可以传换成数组
客户端得到数据后调用该微信API就能实现微信支付了。
10-27统一测试订单与支付接口十
小程序: 支付 模拟器出现二维码 真机直接支付
success:function(res){
var preData=res.data;
console.log(preData);//服务端pay sign参数 下面需要
wx.requestPayment({
timeStamp: preData.timeStamp.toString(),
nonceStr: preData.nonceStr,
package: preData.package,
signType:preData.signType,
paySign: preData.paySign,
success:function(res){
console.log(res.data)
},
fail:function(error){
console.log(error);
}
})
}
返回结果
二维码
order数据表 prepay_id 校验 不行 过不
没有企业 微信sdk 不行 小程序报错
网页 打 错误看 undefined class constant 'MCHID'
没有商户号
10-28支付的服务器端编写(支付结果回调机制)十一
V1
Order
{
//下单接口 下单数据增加记录 复杂 库存量 支付
//1.用户在选择商品后,向AP1提交包含它所选择商品的相关信息
//2.APT在接收到信息后,需要检查订单相关商品的库存量
//3.有库存,把订单数据存入数据库中:下单成功了,返回客户端消息,告诉客户端可以支付了
//4.调用我们的支付接口,进行支付
//5.还需要再次进行库存量检测
//6.服务器这边就可以调用微信的支付接口进行支付
// 小程序根据服务器返回的结果拉起微信支付
//7.微信会返回给我们一个支付的结果(异步) 俩个途径异步服务端 同步客户端
//8.成功:也需要进行库存量的检查
//9.成功:进行库存量的扣除,[微信异步 失败:返回一个支付失败的结果]
}
wx.requestPayment
小程序成功 失败 返回结果
微信通知每隔一段时间通知 异步 微信不能百分百发送到自服务器
自己服务端处理通知了后续不继续
不正确处理还是继续通知 返回微信
Route
{
Route::post('api/:version/pay/notify','api/:version.Pay/receiveNotify');
}
v1
pay
{
//// 小程序根据服务器返回的结果拉起微信支付
// //7.微信会返回给我们一个支付的结果(异步) 俩个途径异步服务端 同步客户端
public function receiveNotify(){
//通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位为秒
// 微信通知每隔一段时间通知 异步 微信不能百分百发送到自服务器
// 自己服务端处理通知了后续不继续
// 不正确处理还是继续通知 返回微信
}
}
10-29支付回调通知处理一
处理什么业务逻辑 注意什么情况
order数据表 status 订单状态
//待支付
const UNPAID=1;
//已支付
const PAID=2;
//已发货
const DELIVERED=3;
//已支付,但库存不足
const PAID_BUT_OUT_OF=4;
Route::post('api/:version/pay/notify','api/:version.Pay/receiveNotify');
不能带参数 返回微信 微信自动过滤多余的
api/:version/pay/notify?param=a
在微信api WxPay.Notify.php 学习 继承重写
Handle xml数组处理
{
public function NotifyProcess($data, &$msg)
{
//TODO 用户基础该类之后需要重写该方法,成功的时候返回true,失败返回false
return true;
}
}
xml 获得 1.post 2.微信sdk
xml 数组 对象 to
sdk自动
//1,检查库存量,超卖
//2,更新这个订单的status状态 支付状态
//3,减库存 有减
//如果成功处理,我们返回微信成功处理的信息,否则,我们需要返回没有成功处理。微信继续通知
//特点:post:xml格式;不会携带参数
其他调用 多步 微信开发 用户 继承重写
v1/pay
{
public function receiveNotify(){
//通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位为秒
// 微信通知每隔一段时间通知 异步 微信不能百分百发送到自服务器
// 自己服务端处理通知了后续不继续
// 不正确处理还是继续通知 返回微信
//8.成功:也需要进行库存量的检查
//9.成功:进行库存量的扣除,[微信异步 失败:返回一个支付失败的结果]
//1,检查库存量,超卖
//2,更新这个订单的status状态 支付状态
//3,减库存 有减
//如果成功处理,我们返回微信成功处理的信息,否则,我们需要返回没有成功处理。微信继续通知
//特点:post:xml格式;不会携带参数
}
}
service
wxnotify
{
1
}
商户处理后同步返回给微信参数:
return_code 不能表示支付成功 成功才有后面
里面的 result_code 才是
pay makeWxPreOrder 设置发给微信的
微信返回的订单号
out_trade_no
eq = =
查询库存量减少count update 但模型有更好方法setDec直接减少 不用查询
模型 setDec('字段',数量)
加呢可能setAsc
Service
WxNotify
{
参考 wxApi已经转换数组
//
//
//
//
//
//
//
//
//
//
//
//
//
// 1
//
//
//
//
//
//
//
//
// 继承重写
public function NotifyProcess($data, &$msg)
{
//wxApi已经转换数组
if ($data['result_code']=='SUCCESS'){
//xml 数组了 result_code 表示支付成功
$orderNo=$data['out_trade_no'];//pay makeWxPreOrder 设置发给微信的 微信返回的订单号out_trade_no
//根据订单号查找相关商品 订单
try{
$order=OrderModel::where('order_no','=',$orderNo)->find();
if ($order->status==OrderStatusEnum::UNPAID){// 1 未支付状态 才继续
//库存量检测
$service=new OrderService();
$stockStatus=$service->checkOrderStock($order->id);//需要订单id
if ($stockStatus['pass']){
//订单通过时
$this->updateOrderStatus($order->id,true);//更新订单状态status
$this->reduceStock($stockStatus);//更新库存量 减少
}else{
//订单不通过 但支付了 不通过就不用减少库存量 只更新支付状态
$this->updateOrderStatus($order->id,false);//status 4
}
}
return true;//告诉微信true 通知处理
}
catch(Exception $ex){
Log::error($ex);
return false;//告诉微信false 继续通知处理
}
}
else{
// //$data['result_code'] 支付不成功
//告诉微信true 通知处理 支付失败处理了 不继续通知 无意义
return true;
}
}
private function updateOrderStatus($orderID,$success){
//更新订单状态status 支付2 支付库存不够4 需要订单号 $success标志位bool
$status=$success?
OrderStatusEnum::PAID :
OrderStatusEnum::PAID_BUT_OUT_OF;
OrderModel::where('id','=',$orderID)->update(['status'=>$status]);
}
private function reduceStock($stockStatus){//更新库存量 减少
//stockStatus['PStatusArray']['count'] 包含 数量
//查询库存量减少count update 但模型有更好方法setDec直接减少 不用查询
foreach ($stockStatus['pStatusArray'] as $singlePStatus){
//$singlePStatus['count']
Product::where('id','=',$singlePStatus['id'])
->setDec('stock',$singlePStatus['count']);
}
}
}
}
TP5 模型实现数据减少 setDec
// 前面是查询 第一个数是写要改变的字段 第二个是要减少的数量
Product::where('id','=',$singlePStatus['id'])->setDec('stock',$singlePStatus['count']);
10-31事务与锁防止多次减库存
多次$stockStatus
微信通知异步
多次操作数据库
if pass 库存量 时 运行快 慢 微信第二次 通知 就多次操作了
概率小 库存量多减少
支付人多时可能
加Db事务处理
单步完 再进二次
Db:startTrans() commit rollback
不能只锁一个 要整体
查询模型时 lock(true) 走完才解锁
语法好了 tp 专业 数据库再学 ->
Service
WxNotify
{
public function NotifyProcess($data, &$msg)
{
//wxApi已经转换数组
if ($data['result_code']=='SUCCESS'){
//xml 数组了 result_code 表示支付成功
$orderNo=$data['out_trade_no'];//pay makeWxPreOrder 设置发给微信的 微信返回的订单号out_trade_no
//根据订单号查找相关商品 订单
Db::startTrans();
try{
$order=OrderModel::where('order_no','=',$orderNo)
->lock(true)//查询锁住 后面完了才解锁
->find();
if ($order->status==OrderStatusEnum::UNPAID){// 1 未支付状态 才继续
//库存量检测
$service=new OrderService();
$stockStatus=$service->checkOrderStock($order->id);//需要订单id
if ($stockStatus['pass']){//运行快 慢 微信第二次 通知 就多次操作了 dedug
//订单通过时
$this->updateOrderStatus($order->id,true);//更新订单状态status
$this->reduceStock($stockStatus);//更新库存量 减少
}else{
//订单不通过 但支付了 不通过就不用减少库存量 只更新支付状态
$this->updateOrderStatus($order->id,false);//status 4
}
}
Db::commit();
return true;//告诉微信true 通知处理
}
catch(Exception $ex){
Db::rollback();
Log::error($ex);
return false;//告诉微信false 继续通知处理
}
}
else{
// //$data['result_code'] 支付不成功
//告诉微信true 通知处理 支付失败处理了 不继续通知 无意义
return true;
}
}
}
数据库锁与事务锁的区别
数据库模型->lock(true)
事务锁 Db
事务锁是等待整个事务提交才会执行第二次事务,但是数据库模型锁只是单步的锁着了数据库查询语句
在后面的操作还没有执行时,数据库模型锁已经放开了
由于微信返回的是xml格式的数据,所以同样可以通过sdk来进行处理。
需要做的是继承WxPayNotify,并且改写里面的NotifyProcess方法。然后调用Handle方法即可。
里面主要的步骤就是,检测库存,减库存,修改订单信息这几步。
新版 微信 api
{
public function NotifyProcess($objData, $config, &$msg)
{
if ($objData['result_code'] == 'SUCCESS') {
$orderNo = $objData['out_trade_no'];
Db::startTrans();
try {
$order = OrderModel::where('order_no', 'eq', $orderNo)->find();
if ($order->status == 1) {
$orderService = new OrderService();
$stockStatus = $orderService->checkOrderStock($order->id);
if ($stockStatus['pass']) {
$this->updateOrderStatus($order->id, true);
$this->reduceStock($stockStatus['pStatusArray']);
} else {
$this->updateOrderStatus($order->id, false);
}
}
Db::commit();
return true;
} catch (Exception $ex) {
Db::rollback();
Log::error($ex);
return false;
}
} else {
return true;
}
}
}
需要返回True来表示接受成功,返回false,微信会隔一段时间再发送一次POST请求。
10-32接收微信回调
不能直接调用NotifyProcess 参数那里来
调用微信api handle
优秀封装
Ngrok 内网转发 新的地址 根目录地址 完整url
其他...
google|反向代理 不安全的 服务端 第三方 微信
过 测试
extra config()
{
'E7epHZhrTfgQ',//创建安全配置文件[盐:随机字符串]
'pay_back_url'=>'http://z.cn/api/v1/pay/notify',//回调地址 微信 通知 本地地址 微信不能访问 真配置云网站
//不配置真网 就用软件 配置 反向代理Ngrok 本地变外网访问到
//Ngrok 内网转发 新的地址 根目录地址 完整url
];
}
Service
pay
{
private function makeWxPreOrder($totalPrice){
//微信预订单
//openid 不是自己sql的UID
$openid=Token::getCurrentTokenVar('openid');
if (!$openid){
throw new TokenException();
}
//wx-sdk 统一下单类 WxPay.Data.php ->WxPayUnifiedOrder
$wxOrderData=new \WxPayUnifiedOrder();//没命名空间 要加 \
$wxOrderData->SetOut_trade_no($this->orderNO);//订单号
$wxOrderData->SetTrade_type('JSAPI');//交易类型
$wxOrderData->SetTotal_fee($totalPrice*100);//总金额分为单位 $status里有元为单位
$wxOrderData->SetBody('零食商贩');//商品简单描述
$wxOrderData->SetOpenid($openid);//用户标识
$wxOrderData->SetNotify_url(config('secure.pay_back_url'));//在配置里设置 微信回调url 异步接收 通知 测试随便个
return $this->getPaySignature($wxOrderData);
}
}
v1/pay
{
public function receiveNotify(){
//通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位为秒
// 微信通知每隔一段时间通知 异步 微信不能百分百发送到自服务器
// 自己服务端处理通知了后续不继续
// 不正确处理还是继续通知 返回微信
//8.成功:也需要进行库存量的检查
//9.成功:进行库存量的扣除,[微信异步 失败:返回一个支付失败的结果]
//1,检查库存量,超卖
//2,更新这个订单的status状态 支付状态
//3,减库存 有减
//如果成功处理,我们返回微信成功处理的信息,否则,我们需要返回没有成功处理。微信继续通知
//特点:post:xml格式;不会携带参数
$notify=new WxNotify();
$notify->Handle();//不能直接调用NotifyProcess 参数那里来
//微信怎么知道那里调用 回调地址 PayService $wxOrderData->SetNotify_url
}
}
外部网址使用
要从根目录一直到 index.php
后面才是路由
www.yhf7/zerg/public/index.php/api/v1/pay/notify
微信api不行 Ngrok不行 过
10-33测试支付全流程
其他测试过
分开测试 先makeWxPreOrder
WxPay.Api.php 没有引入 Notify.php
require_once "WxPay.Notify.php";
post http://localhost/zerg/public/index.php/api/v1/pay/notify
post http://z.cn/api/v1/pay/notify
未定义数组索引:http_raw_post_data 出现就正确
body raw 随便打 其他问题simplexml_load_string(): Entity: line 1: parser error : Start tag expected, '<' not found
小程序 微信工具 令牌 支付 扫描
支付改下 金额 products
检查 数据库 order表 status字段支付状态 2-已支付
products stock库存量 减少
黑盒测试 products stock库存量0 没有 测试其他接口 方法 失败时
订单不通过 不能直接 先支付 再改stock库存量
order表 status字段支付状态 4-已支付但没库存量
stock 原来
可以不可以 断点调试
pay_back_url 加xdebug 但是微信会 忽略参数
复杂 断点调试 转发 在测试
10-34可以断点调试微信支付的回调吗?
postman 测试
post http://localhost/zerg/public/index.php/api/v1/pay/notify
post http://z.cn/api/v1/pay/notify
跳转到 redirectNotify
xdebug 测试
小程序 测试
支付 微信回调 进入 redirectNotify 通知回调
卡在断点 微信通知 多次
为什么 处理了微信通知 还继续断点
数据库 支付状态
继续redirectNotify
因为 转发测试 receiveNotify 没有return 微信处理通知结果true 继续
微信api 企业不行 过
v1/pay
{
public function receiveNotify(){
// $notify=new WxNotify();
// $notify->Handle();//不能直接调用NotifyProcess 参数那里来
//微信怎么知道那里调用 回调地址 PayService $wxOrderData->SetNotify_url http://z.cn/api/v1/pay/notify
//转发调试 因为微信过滤参数 无法调试 屏蔽自己 测试用 发布改回
$xmlData=file_get_contents('php://input');
$result=curl_post_raw('http://z.cn/api/v1/pay/re_notify?XDEBUG_SESSION_START=15687',$xmlData);//common里的方法
// return true;因为 转发测试 receiveNotify 没有return 微信处理通知结果true 继续
}
public function redirectNotify(){
$notify=new WxNotify(); //断点
$notify->Handle();
}
}
Service
WxNotify
{
if ($order->status==OrderStatusEnum::UNPAID)// 1 未支付状态 才继续 //debug
$service=new OrderService();//debug
}
;因为 转发测试 receiveNotify 没有return 微信处理通知结果true 继续
多次断点
route
Route::post('api/:version/pay/re_notify','api/:version.Pay/redirectNotify');
common.php
{
function curl_post_raw($url, $rawData)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_POSTFIELDS, $rawData);
curl_setopt(
$ch, CURLOPT_HTTPHEADER,
array(
'Content-Type: text' //传的不是json 微信返的xml
)
);
$data = curl_exec($ch);
curl_close($ch);
return ($data);
}
}
10-35TP5中的分页查询与获取用户历史订单数据
我的
我的订单 列表 分页 多个
支付 和 未
order 订单 不能全部 分页
时间倒序 新在前面
ctrl+b 源码 学习 注释看 参数
{
/**
* 分页查询
* @param int|array $listRows 每页数量 数组表示配置参数
* @param int|bool $simple 是否简洁模式或者总记录数
* @param array $config 配置参数
* page:当前页,
* path:url路径,
* query:url额外参数,
* fragment:url锚点,
* var_page:分页变量,
* list_rows:每页数量
* type:分页类名
* @return \think\Paginator
* @throws DbException
*/
public function paginate($listRows = null, $simple = false, $config = [])
}
tp5 到时系统 过
简洁模式 不需要数目 下滑加载 优先
传统 参数数目
route
Route::get('api/:version/order/by_user','api/:version.Order/getSummaryByUser');
v1/order
{
public function getSummaryByUser($page=1,$size=15){
//我的订单 列表 分页 多个 默认一页 15个订单
//route validate参数正整数 token-uid
(new PagingParameter())->goCheck();
$uid=\app\api\service\Token::getCurrentUid();//uid
//复杂 写到 model Order
$pagingOrders=\app\api\model\Order::getSummaryByUser($uid,$page,$size);
//返回的是paginate对象 对象判断空isEmtry
if ($pagingOrders->isEmpty()){
//return null; throw new Exception
return [
'data'=>[],
'current_page'=>$pagingOrders->getCurrentPage(),//$page paginate对象获取当前页
];
}
//$pagingOrders不空时
$data=$pagingOrders->toArray();
return [
'data'=>$data,
'current_page'=>$pagingOrders->getCurrentPage()
];
}
}
model
order
{
order('create_time','desc')//倒序
//数目 简洁模式 多少页
->paginate($size,true,['page'=>$page]);//tp5分页查询 源码 参数 返回paginate对象
// ->select();//查询全部 paginate同find select
return $pagingData;
}
}
}
validate
{
'isPostiveInt',
'size'=>'isPostiveInt'
];
protected $message=[
'page'=>'分页参数必须是正整数',
'size'=>'分页参数必须是正整数',
];
}
}
模型分页查询(paginate)
第一个参数是分类数
第二个数是否简洁模式
第三个是数组填入分页数
{
public static function getSummaryByUser ($uid,$page=1,$size=15) {
$paginData = self::where('user_id','=',$uid)->order('create_time desc')->paginate($size,true,['page' => $page]);
return $paginData;
}
}
10-36测试与验证历史订单数据接口
测试
postman
get http://z.cn/api/v1/order/by_user?page=1&size=3
headers
token 'token' 小程序小工具获取
需要带令牌
多重数组 返回客户端 都要是json 不是json字符串
权限控制 用户管理员都可以访问 订单
v1/order
{
protected $beforeActionList=[//前置操作
'checkExclusiveScope'=>['only'=>'placeorder'],//要小写createOrUpdateAddress
'checkPrimaryScope'=>['only'=>'getsummarybyuser'],//权限控制 用户管理员都可以访问 历史订单
];
public function getSummaryByUser($page=1,$size=15){
//我的订单 列表 分页 多个 默认一页 15个订单
//route validate参数正整数 token-uid
(new PagingParameter())->goCheck();
$uid=\app\api\service\Token::getCurrentUid();//uid
//复杂 写到 model Order
$pagingOrders=\app\api\model\Order::getSummaryByUser($uid,$page,$size);
//返回的是paginate对象 对象判断空isEmtry
if ($pagingOrders->isEmpty()){
//return null; throw new Exception
return [
'data'=>[],
'current_page'=>$pagingOrders->getCurrentPage(),//$page paginate对象获取当前页
];
}
//$pagingOrders不空时
$data=$pagingOrders->hidden(['snap_items','snap_address','prepay_id'])//next_items不隐藏 data隐藏 我的订单概况不用 进入才要
->toArray();
return [
'data'=>$data,
'current_page'=>$pagingOrders->getCurrentPage()//多余 测试x对象 $data里面有
];
}
}
//next_items不隐藏 data隐藏 我的订单概况不用 进入才要
->toArray();
测试 加debug 再删除 就行了
10-37订单详情接口
订单详情
order表里面
http://z.cn/api/v1/order/3
postman
headers
token ''
v1/order
{
protected $beforeActionList=[//前置操作
'checkExclusiveScope'=>['only'=>'placeorder'],//要小写createOrUpdateAddress
'checkPrimaryScope'=>['only'=>'getdetail,getsummarybyuser'],//权限控制 用户管理员都可以访问 历史订单
];
//订单详情
public function getDetail($id){
(new IDMustBePostiveInt())->goCheck();
$orderDetail=\app\api\model\Order::get($id);
if (!$orderDetail){
throw new OrderException();
}
return $orderDetail->hidden(['prepay_id']);
}
}
route
{
Route::get('api/:version/order/:id','api/:version.Order/getDetail',[],['id'=>'\d+']);
}
http://z.cn/api/v1/order/3
snap_items
snap_address
json字符串 to json
模型用读取器 来
model 模型 读取器 改 其他地方调用 都json
order
{
//读取器 json字符串tojson
public function getSnapItemsAttr($value){
if (empty($value)){
return null;
}
return json_decode($value);//json字符串tojson
}
public function getSnapAddressAttr($value){
if (empty($value)){
return null;
}
return json_decode($value);
}
}
D:\PanDownload\【微信小程序】\微信小程序商城构建全栈应用\
第11章前端部分:前端框架构建与令牌管理
11-1小程序分开场白与新建小程序项目
配套 小程序各个入门-> 系统->
微信小程序api 灵活应用
Protoss-tool
Protoss
11-2新建首页页面与小程序中的MVC
首页 大概轮廓
版本 自动
app.json 自动
pages先
手动导入app.json
pages
home 首页 新pages自动home js html css ...版本不行
先app.json{["pages":[]]} 新pages自动home
不管智能
版本
{
"pages": [
"pages/home/home" 不是/开头
]
}
old +/ 可以
前端 框架 布局 重要
首页 大概俩个区域
轮播图
成品
轮播图 先加载数据 再绑定
home.js 相当 控制器
再加一个home-model.js model模型
现在前端处理一些后端数据 多了 分层 复杂
11-3使用ES6构建面向对象的JS代码及访问服务器API获取数据
面向对象 home-model.js 类
es6 类
微信小程序api 大部分 异步
var b=wx.xx 同步
wx.xx 异步
success 方法接收 回调函数
不要直接onload
_loadData 方法加载
工具设置 不检测合法域名
合法域名
模拟器
真机 必须是 https 域名完成备案
小程序设置 网站管理
设置
服务器域名
request合法域名 http://z.cn
https request 其他到时
home-model.js
{
//es6 语法 类
class Home{
constructor(){
}
getBannerData(id){//加载banner数据
wx.request({
url: 'http://z.cn/api/v1/banner/'+id,
method:"GET",
success:function(res){
console.log(res)
return res.data;
}
})
}
}
export {Home}
}
home.js
{
// var Home=require('./home-model.js');微信自带导入js
import {Home} from 'home-model.js';//es6 oop
var home =new Home();
onLoad: function (options) {
this._loadData();//不直接调用
},
/**
* 加载home-model.js数据
*/
_loadData:function(){
var id=1;
var data=home.getBannerData(id);
},
}
11-4异步回调与箭头函数
回调方法 名字 等同 好注释
异步在异步
不断 异步定义
old 稳定 new
getBannerData(id,callBack){//加载banner数据
wx.request({
url: 'http://z.cn/api/v1/banner/'+id,
method:"GET",
success:function(res){
// console.log(res)
// return res.data;
callBack(res);
}
})
}
_loadData:function(){
var id=1;
// var data=home.getBannerData(id,this.callBack);回调方法
var data = home.getBannerData(id, (res)=>{console.log(res)});//es6异步 简单这样
// console.log(data)不能输出 异步 不能var data同步方式 回调办法
},
callBack:function(res){
//负责逻辑用
console.log(res);
},
由于不是同步,直接相等得不到结果,需要调用回调函数
解决办法:再定义一个回调函数,把回调函数作为参数传到getBannerData里面去,当成功取到结果之后,在success方法里面调用回调函数,从而把结果传入home.js里面。
11-5构建请求基类(重要)
多请求 封装 基类 多次令牌...
v1 v2 该 俩版本
if(params.sCallBack){
params.sCallBack(res);
}
==================
params.sCallBack&¶ms.sCallBack(res);//相当于if语句
bool 1:1 0:1
Config 静态调用
不用 import export new x() 实例化
utils/
base.js
{
import {Config} from '../utils/config.js'
class Base{
constructor(){
// this.baseRequestUrl="http://z.cn/api/v1/"
//静态调用
this.baseRequestUrl=Config.restUrl;
}
request(params){//params数组对象
var url = this.baseRequestUrl + params.url;
if(!params.type){
params.type="GET";
}
wx.request({
url: url,
data:params.data,
method:params.type,
header:{
'content-type':'application/json',
'token':wx.getStorageSync('token')
},//Token
success:function(res){
// if(params.sCallBack){
// params.sCallBack(res);
// }
params.sCallBack&¶ms.sCallBack(res);
},
fail:function(err){
console.log(err)
}
})
}
}
}
config.js
{
class Config{
constructor(){
}
}
Config.restUrl='http://z.cn/api/v1/';//对象添加 静态
这样写在类的外边是为了避免在调用的时候实例化
export{Config};
}
由于http请求用到的很多,所以建一个基类,保存request方法
utils.base.js
由于这个baseUrl用到的很多,不如放到配置文件里面。
urils\config.js
11-6请求基类的应用
基类 res.data 其他用
子类 res.item 特定对象可以简化
home-model.js
{
//es6 语法 类
// import {Base} from '/utils/base.js';
import { Base } from '../../utils/base.js';
//../相对 /home绝对
class Home extends Base{
constructor(){
super();//调用父类构造
}
getBannerData(id,callBack){//加载banner数据
var params={
url:'banner/'+id,
// data:null,
// type:'GET',
sCallBack:function(res){
callBack&&callBack(res.items);
}
// sCallBack:callBack
}
this.request(params);
// wx.request({
// url: 'http://z.cn/api/v1/banner/'+id,
// method:"GET",
// success:function(res){
// // console.log(res)
// // return res.data;
// callBack(res);
// }
// })
}
}
export {Home}
}
utils/base
{
import {Config} from '../utils/config.js'
class Base{
constructor(){
// this.baseRequestUrl="http://z.cn/api/v1/"
//静态调用
this.baseRequestUrl=Config.restUrl;
}
request(params){//params数组对象
var url = this.baseRequestUrl + params.url;
if(!params.type){
params.type="GET";
}
wx.request({
url: url,
data:params.data,
method:params.type,
header:{
'content-type':'application/json',
'token':wx.getStorageSync('token')
},//Token
success:function(res){
// if(params.sCallBack){
// params.sCallBack(res);
// }
params.sCallBack&¶ms.sCallBack(res.data);
},
fail:function(err){
console.log(err)
}
})
}
}
export{Base}
}
//继承基类
;//基类构造函数
//因为所有的都是只要这个数据即可,所以只在回调函数中返回res.data
11-7使用数据绑定将数据显示在UI上
dom 没
数据绑定
image mode="aspectFill" 模式填充
home.js
{
var data = home.getBannerData(id, (res)=>{
console.log(res)
this.setData({ "bannerArr": res })
});
}
wxml
{
测试
}
wxss css
width: 100%;
height: 400rpx;
11-8商品主题UI与数据加载
小程序 多开
精选主题
名字
图片
最近新品
配套 直接 用 css
以后系统css 学习
css @import "x.xwss"
base.wxss 封装 其他调用 模板用
home.wxss
tpls/base.wxss
tpls/products/ *
一个单词 callback
多个 才 callBack
循环 几个 什么 类名 什么样式
home.wxml
{
精选主题
}
css
home.js
{
_loadData
{
home.getThemeData((res) => {
// console.log(res[0])
this.setData({
'themeArr': res
})
})
}
}
home-model.js
{
//首页主题
getThemeData(callBack){
var params={
url:'theme?ids=1,2,3',
sCallBack: function (res) {
// console.log(res);
callBack && callBack(res);
}
}
this.request(params);
}
}
11-9wxif的应用
循环 几个 什么 类名 什么样式
12同
3不同
wx:if=
wx:ifel=
wx:else
class="theme-item.big" 一个
class="theme-item big" 2个
精选主题
11-10小程序模板的分析与应用
图片不同 其他相同 考虑 用模板写
模板
一个一个模板 一个小格 灵活
整体 方便 配套 其他也可以用专题栏目一...
嵌套模板 大一个 里面小格一个
单双标签
模板html
引用
name
css wxss
@import "../tpls/products/products-tpl.wxss";
{
{{item.name}}
¥{{item.price}}
}
模板 不支持 js
其他...可能支 到时
模板 不直接 加载数据
调用 加载 js 模板is data={{传递到模板}}
不换行
换行
字体 全局好 当前css
宋体 放大不够好
home.wxml
{
最近新品
}
home-model.js
{
//产品
getProductsData(callBack){
var params={
url:'product/recent',
sCallBack: function (res) {
// console.log(res);
callBack && callBack(res);
}
};
this.request(params);
}
}
home.js
{
_loadData
{
home.getProductsData((res)=>{
console.log(res)
this.setData({
productsArr:res
})
})
}
}
11-11全局样式与复用思想
全局样式 app.wxss 自动 不用 @import 'a.wxss'
base.js 模板 复用思想
前端 复用 布局
先写 重构 搞定重复内容
次数多 经验多 思路清晰
最外层 view
11-12页面跳转11-12
用户交互
点击 图片 跳转 标识 1 2 3
工具 wx
控制台 wxml 源码
商品详情
pages/product
新建 page product自动 js html css json
app.json pages 自动注册
wx.navigateTo({
url: '../product/product', 要前缀 不后缀
})
home.wxml
{
}
home.js
{
/**
* 点击轮播图跳转到商品详情
*/
onProductsItemTap:function(event){
console.log(event.currentTarget.dataset.id);
var id = event.currentTarget.dataset.id;
wx.navigateTo({
url: '../product/product?id='+id,
})
},
}
11-13页面间传递与接收参数
event.currentTarget.dataset.id;
封装 base.js
/*获得元素上的绑定的值 */
getDataSet(event,key){
return event.currentTarget.dataset[key];
}
精选主题
调整到/pages/theme
主题专题
精选主题
data-id="{{item.id}}" data-name="{{item.name}}"
id 和 主题名字
product.js
{
onLoad: function (options) {
var id=options.id;//获取 点swiper传来的id标识
console.log("product:"+id);
},
}
最近新品
要绑定到模板里面
单个商品
轮播图 和 最近新品 都是跳转同一个地方
theme.js
{
onLoad: function (options) {
var id=options.id;//获取 点swiper传来的id标识
console.log("product:"+id);
},
}
home.js
{
/**
* 点击轮播图跳转到商品详情
* 轮播图 和 最近新品 都是跳转同一个地方
*/
onProductsItemTap:function(event){
// console.log(event.currentTarget.dataset.id);
// var id = event.currentTarget.dataset.id;//封装base.js
var id =home.getDataSet(event,'id');
// console.log(id)
wx.navigateTo({
url: '../product/product?id='+id,
})
},
/**
* 精选主题 跳转 theme
*/
onThemesItemTap:function(event){
var id=home.getDataSet(event,'id');
var name=home.getDataSet(event,'name');
wx.navigateTo({
url: '../theme/theme?id='+id+"&name="+name,
})
},
}
// import {Base} from '../../utils/base.js'
// var base=new Base(); home继承了base
home.wxml
{
精选主题
最近新品
}
11-14window与tarbar的配置
app.json 全局 pages.json可覆盖
"window" 小程序顶部 颜色和 主题名
配置设置
"window":{
"navigationBarTitleText": "小程序",
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#ab956d",
"backgroundColor": "#eeeeee",
"backgroundTextStyle": "light",
"enablePullDownRefresh": false
},
底部tab切换 最少2个 最多5个
app.json 不能注释 ,
"tabBar" 跳转路径 图片 名字
"tabBar": {
"list": [
{
"pagePath": "pages/home/home", 跳转那
"text": "主页",名字
"iconPath": "imgs/toolbar/home.png",未选择时图片
"selectedIconPath": "imgs/toolbar/home@selected.png"选择时图片
},
},
"tabBar": {
"list": [
{
"pagePath": "pages/home/home",
"text": "主页",
"iconPath": "imgs/toolbar/home.png",
"selectedIconPath": "imgs/toolbar/home@selected.png"
},
{
"pagePath": "pages/category/category",
"iconPath": "imgs/toolbar/category.png",
"selectedIconPath": "imgs/toolbar/category@selected.png",
"text": "分类"
},
{
"pagePath": "pages/cart/cart",
"iconPath": "imgs/toolbar/cart.png",
"selectedIconPath": "imgs/toolbar/cart@selected.png",
"text": "购物车"
},
{
"pagePath": "pages/my/my",
"iconPath": "imgs/toolbar/my.png",
"selectedIconPath": "imgs/toolbar/my@selected.png",
"text": "我的"
}
],
"backgroundColor": "#F5F5F5", 背景颜色
"selectedColor": "#AB956D", 选择颜色
"color": "#989898", 字体颜色
"borderStyle": "white", 样式风格 tar上面一个小黑线
"position": "bottom" 底部tab
},
icon 图片 美工 diyPs 网上素材
放小程序 里小 不用服务器 加载快
about 没有 新 源码看看
图片 命名 枚举 home@selected
app.json
{
{
"pages": [
"pages/home/home",
"pages/product/product",
"pages/theme/theme",
"pages/category/category",
"pages/cart/cart",
"pages/my/my"
],
"window": {
"navigationBarTitleText": "小程序",
"navigationBarTextStyle": "white",
"navigationBarBackgroundColor": "#ab956d",
"backgroundColor": "#eeeeee",
"backgroundTextStyle": "light",
"enablePullDownRefresh": false
},
"tabBar": {
"list": [
{
"pagePath": "pages/home/home",
"text": "主页",
"iconPath": "imgs/toolbar/home.png",
"selectedIconPath": "imgs/toolbar/home@selected.png"
},
{
"pagePath": "pages/category/category",
"iconPath": "imgs/toolbar/category.png",
"selectedIconPath": "imgs/toolbar/category@selected.png",
"text": "分类"
},
{
"pagePath": "pages/cart/cart",
"iconPath": "imgs/toolbar/cart.png",
"selectedIconPath": "imgs/toolbar/cart@selected.png",
"text": "购物车"
},
{
"pagePath": "pages/my/my",
"iconPath": "imgs/toolbar/my.png",
"selectedIconPath": "imgs/toolbar/my@selected.png",
"text": "我的"
}
],
"backgroundColor": "#F5F5F5",
"selectedColor": "#AB956D",
"color": "#989898",
"borderStyle": "white",
"position": "bottom"
},
"sitemapLocation": "sitemap.json"
}
}
11-15主题页面与数据
"backgroundColor": "#F5F5F5", 背景颜色
"selectedColor": "#AB956D", 字体选择颜色
"color": "#989898", 字体颜色 未选择
"borderStyle": "white", 样式风格 tar上面一个小黑线
"position": "bottom" 底部tab
专题详情 页面 theme
分上下
图片 头图
模板 复用
this._loadData(id); _loadData:function(id) 一
this.data.id 二 保存到data 配套
export 问题
template模板 问题 data='{{key:valueData里的名}}'
里面 对应 key
模板导入不成功
debug 在模板里面 随便打个字调试
debug 控制台 appData 重要调试
数据绑定都显示在 appData
wxml
{
}
theme-model.js
{
import { Base } from '../../utils/base.js';
class Theme extends Base{
constructor(){
super();
}
//获取主题下的商品列表
//对应主题的id号
getProductsData(id,callBack) {
var params = {
url: 'theme/'+id,
sCallBack: function (res) {
// console.log(res);
callBack && callBack(res);
}
};
this.request(params);
}
}
export{Theme};
}
theme.js
{
// pages/theme/theme.js
import {Theme} from 'theme-model.js';
var theme=new Theme();//实例化 主题列表对象
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
var id=options.id;
var name=options.name;
// console.log(id,name)
this.data.id=id;
this.data.name=name;
// this.setData({
// id:this.data.id,
// name:this.data.name
// })
this._loadData();
},
_loadData:function(){
theme.getProductsData(this.data.id,(res)=>{
this.setData({ themeInfo: res })
});
}
})
}
11-16动态设置导航栏标题
home.json
{
"navigationBarTitleText": "零食商贩"
}
只能页面 固定 要根据服务器名 动态改变
theme.js
//动态设置标题
wx.setNavigationBarTitle({
title:name,
})
数据库 的值
theme name
onload 设置可能延长
onReady 显示画面时 就加载 官网推荐 小程序框架
onReady:function(){
//动态设置标题
wx.setNavigationBarTitle({
title:this.data.name,
})
},
延迟 小技巧 app.json 设置空 空到加载 合理
11-17商品闲情页面一
商品详情 页面 有价格 介绍 那个
pages/product
淘宝 电商 到时
产品图片 css快但不 图片慢用(缓存加快)
data: {
id:null 类似初始化
},
布局 wxml
俩大部分
1.图片 数量 价格
购物车icon
图片
数量
数量 上下分割 加入 3部分
价格 4部分
2.详情tab
css 用 ->
购物车 动态 复杂 ->
定思维
wxss
product.js
{
// pages/product/product.js
import {Product} from 'product-model.js'
var product=new Product();
Page({
/**
* 页面的初始数据
*/
data: {
id: null,//类似初始化
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
var id=options.id;//获取 点swiper传来的id标识
// console.log("product:"+id);
this.data.id=id;//类似成员变量
this._loadData();
},
_loadData:function(){
product.getDetailInfo(this.data.id,(res)=>{
console.log(res)
this.setData({
product:res
})
})
}
})
}
product-model.js
{
import { Base } from '../../utils/base.js';
class Product extends Base{
constructor(){
super();
}
getDetailInfo(id,callBack){
var param={
url:'product/'+id,
sCallBack:function(res){
callBack&&callBack(res);
}
}
this.request(param);
}
}
export {Product}
}
product.wxml
{
}
11-18商品详情页面二Picker组件详解
range数组里面多少个 数据绑定
value数组下标
选择 标签
check
数量 3部分
数量
文字 数量 图片
上下分割
加入
加view text text image
picker 用了flex布局
交互 定式思维
不是bindtap
picker 改变 用 bindchange="bindPickerChange"
bindchange 改变时 文档看
js string转int
Number(str)
str.parseInt()
方法一 转换
// console.log(event); detail vlaue 3+1实际
var count=event.detail.value;
// console.log(Number(count)) value字符串 转数字+1
this.setData({
productCounts: Number(count)+1
})
方法二 数组上下标
var index=event.detail.value;
var selectedCount=this.data.countsArray[index];
this.setData({
productCounts:selectedCount
})
如果 没有库存量 禁止选择 用class实现 禁止
默认 选择
data:{
productCounts:1,
}
product.js
{
bindPickerChange:function(event){
// // console.log(event); detail vlaue 3+1实际
// var count=event.detail.value;
// // console.log(Number(count)) value字符串 转数字+1
// this.setData({
// productCounts: Number(count)+1
// })
var index=event.detail.value;
var selectedCount=this.data.countsArray[index];
this.setData({
productCounts:selectedCount
})
}
}
wxml
{
数量
{{productCounts}}
}
11-19商品详情页面三自定义选项卡切换思路
数量 3部分
数量
文字 数量 图片
上下分割
加入购物车 也库存量 禁止
购物车 交互 js 特效 提交 -> 复杂
价格部分 class="basic-info-box"
详细tabs 2部分
信息名字 3
1.text固定
2.wx:for 循环数组 点击跳转 bindtap绑定事件 点击那个 加class区别选择
建议写到js data
详细内容 3对应
{{item}}
保存 默认打开 首页 多次打开麻烦 这样好调试
设置编译
保存路径 当前 pages/product/product
默认选择
新版自动 加 模拟更新
或者app.json 优先级
最好设置参数 不然 出问题 id=1
详细内容 3对应 product-detail-box
动态选择 显示 点击 那个显示那个
detail
view hidden=true
show=true
wx:if elif else
{
detail
param
sale
}
hidden=true
{
detail
param
sale
}
js
{
// pages/product/product.js
import {Product} from 'product-model.js'
var product=new Product();
Page({
/**
* 页面的初始数据
*/
data: {
id: null,//类似初始化
countsArray:[1,2,3,4,5,6,7,8,9,10],//picker
productCounts:1,//默认选择
currentTabsIndex:0,
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
var id=options.id;//获取 点swiper传来的id标识
// console.log("product:"+id);
this.data.id=id;//类似成员变量
this._loadData();
},
_loadData:function(){
product.getDetailInfo(this.data.id,(res)=>{
console.log(res)
this.setData({
product:res
})
})
},
bindPickerChange:function(event){
// // console.log(event); detail vlaue 3+1实际
// var count=event.detail.value;
// // console.log(Number(count)) value字符串 转数字+1
// this.setData({
// productCounts: Number(count)+1
// })
var index=event.detail.value;
var selectedCount=this.data.countsArray[index];
this.setData({
productCounts:selectedCount
})
},
onTabsItemTap:function(event){
var index=product.getDataSet(event,'index');
// console.log(index)
this.setData({
currentTabsIndex:index
});
}
})
}
html
{
{{item}}
detail
param
sale
}
11-20商品详情页面四
只能一个img 详细全 id=11
banner 贵妃笑
product 同一个数据绑定里面
console.log 调试
AppData 数据绑定 查看
销后保障 简单 自己 扩展
html
{
{{item}}
{{item.name}}
{{item.detail}}
七天无理由免费退货
}
11-21商品分类页面一
分类
pages/category/category
布局
左 点击左加载右
右
上图片
下product模板
手机web 比电脑web 简单多
前端 多写 网页 交互
在加一个view 布局 不用上下 左右
css 不同人不同 没有固定 写法
布局 css
css
html
{
}
js
{
// pages/category/category.js
import {Category} from 'category-model.js';
var category=new Category();
Page({
/**
* 页面的初始数据
*/
data: {
id:2
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
this._loadData();
},
_loadData:function(){
category.getCategoryType((res)=>{
console.log(res);
this.setData({
categoryTypeArr:res
})
})
category.getProductsByCategory(this.data.id,(res) => {
console.log(res);
this.setData({
category: res
})
})
},
click:function(event){
// console.log(event)
var id=category.getDataSet(event,'id');
console.log(id)
this.setData({
selectId:id
})
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
},
})
}
model.js
{
import { Base } from '../../utils/base.js';
class Category extends Base{
constructor(){
super();
}
/*获得所有分类*/
getCategoryType(callBack){
var param={
url:'category/all',
sCallBack:function(res){
callBack&&callBack(res);
}
}
this.request(param);
}
/*获得某种分类的商品*/
getProductsByCategory(id,callBack){
var param = {
url: 'product/by_category?id='+id,
sCallBack: function (res) {
callBack && callBack(res);
}
}
this.request(param);
}
}
export {Category};
}
11-22商品分类页面二
分类 默认点击
一次性加载 慢 卡
模板优势 其他提供 自己参数 就可以直接用 舒服 方便
js css html 都可以 封装
模板试 开发
模板
/tpls/category/ *
wxml wxss
新其他源码 ->下
html
{
}
js
{
// pages/category/category.js
import {Category} from 'category-model.js';
var category=new Category();
Page({
/**
* 页面的初始数据
*/
data: {
id:2,
index:0
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
this._loadData();
},
_loadData:function(){
// var that=this;
category.getCategoryType((categoryData)=>{
console.log(categoryData);
this.setData({
categoryTypeArr: categoryData
});
//一定在回调里再进行获取分类详情的方法调用
// console.log(categoryData[this.data.index].id)
category.getProductsByCategory(categoryData[this.data.index].id, (res) => {
// console.log(res);
var dataObj={
products:res,
topImgUrl: categoryData[this.data.index].img.url,
title: categoryData[this.data.index].name
};
this.setData({
categoryProducts: dataObj
});
// console.log(dataObj)
})
})
},
click:function(event){
// console.log(event)
var id=category.getDataSet(event,'id');
var index = category.getDataSet(event, 'index');
console.log(id,index)
this.setData({
id:id,
index:index
})
console.log(this.data.categoryProducts)
},
onProductsItemTap:function(event){
var id = id = category.getDataSet(event, 'id');
wx.navigateTo({
url: '../product/product?id=' + id,
})
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
},
})
}
model.js
{
import { Base } from '../../utils/base.js';
class Category extends Base{
constructor(){
super();
}
/*获得所有分类*/
getCategoryType(callBack){
var param={
url:'category/all',
sCallBack:function(res){
callBack&&callBack(res);
}
}
this.request(param);
}
/*获得某种分类的商品*/
getProductsByCategory(id,callBack){
var param = {
url: 'product/by_category?id='+id,
sCallBack: function (res) {
callBack && callBack(res);
}
}
this.request(param);
}
}
export {Category};
}
11-23数据从服务器到前端交互的流程总结(重要)
web 到时
分类 轮播图 广告 详情 ...
一次性 加载 不用多次服务器请求
单步 需要 多次
api^v单步 需要 多次
js
数据绑定v ^event回传自定义属性 v响应根据回传属性重新做数据绑定
显示数据 wxml 标签自定义属性数据date-*
js
{
// pages/category/category.js
import {Category} from 'category-model.js';
var category=new Category();
Page({
/**
* 页面的初始数据
*/
data: {
id:2,
index:0
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
this._loadData();
},
_loadData:function(){
// var that=this;
category.getCategoryType((categoryData)=>{
console.log(categoryData);
this.setData({
categoryTypeArr: categoryData
});
//一定在回调里再进行获取分类详情的方法调用
// console.log(categoryData[this.data.index].id)
category.getProductsByCategory(categoryData[this.data.index].id, (res) => {
// console.log(res);
var dataObj={
products:res,
topImgUrl: categoryData[this.data.index].img.url,
title: categoryData[this.data.index].name
};
this.setData({
categoryProducts: dataObj
});
// console.log(dataObj)
})
})
},
click:function(event){
// console.log(event)
var id=category.getDataSet(event,'id');
var index = category.getDataSet(event, 'index');
console.log(id,index)
this.setData({
id:id,
index:index
})
this._loadData();//重新加载
console.log(this.data.categoryProducts)
},
onProductsItemTap:function(event){
var id = id = category.getDataSet(event, 'id');
wx.navigateTo({
url: '../product/product?id=' + id,
})
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
},
})
}
点击name
_loadData 重新加载
css 点击动画 name
tanslate
类class
{{'tanslate'+index}}
wxml
{
}
D:\PanDownload\【微信小程序】\微信小程序商城构建全栈应用\
第12章 购物车
12-1以面向对象的方式编写购物车代码
购物车
/pages/cart/cart
购物车来源 /pages/product 本地缓存
复杂 model模型的优势
加入到购物车
如果之前没有这样的商品,则直接添加一条新的记录,数量为counts如果有,则只将相应数量+ counts
@params:item-{obj}商品对象,counts-{int}商品数目,
add(item,counts)
model.js
{
import { Base } from '../../utils/base.js';
class Cart extends Base{
constructor(){
super();
this._storageKeyName='cart';//缓存区分key 其他token
}
/**
* 加入到购物车
* 如果之前没有这样的商品,则直接添加一条新的记录,数量为counts
* 如果有,则只将相应数量+ counts
* @params:item-{obj}商品对象,counts-{int}商品数目,
*/
add(item,counts){
var cartData=this.getCartDataFromLocal();
var isHasInfo=this._isHasThatOne(item.id,cartData);//item.id动态语言 随意添加删除 不|灵活方便
if(isHasInfo.index==-1){
item.counts = counts;//有更新 没有新增
item.selectStatus=true;//设置是否选中状态 业务需要 购物车是否选中状态
cartData[isHasInfo.index].counts+=counts;
}
wx.setStorageSync(this._storageKeyName, cartData);//更新缓存
}
//从缓存里读取数据 item counts 有更新 没有新增
//从缓存里读取购物车数据
getCartDataFromLocal(flag){
var res=wx.getStorageInfoSync(this._storageKeyName);
if(!res){
res=[];
}
return res;
}
/**
* 判断某个商品是否已经被添加到购物车中,并且返回这个商品的
* 数据以及所在数组中的序号
*/
_isHasThatOne(id,arr){//_表示私有方法区分
//没数据 先空 假设完 继续其他 有再来编写 技巧
//判断商品 和数量 数据库 api 查询
//item数组 一系列 item
var item,result={index:-1};
for(let i=0;i
加入购物车
商品详情里的
购物车 显示数量 动态 icon
product.js可以 不够好 缓存读取|点击数量时加
在 cart-model.js 里写 购物车 获取详情方法
getCartTotalCounts 缓存读取
cart-model.js -product.js -product.wxml
自己 和其他模型类 使用
调试 控制台 storage
小程序调试
console
数量不显示 stroage viewicon
小程序页面调试 // debugger;//debug不行
控制台 Sources
cart cart-model.js [tm] 带[] 里断点调试
版本 pages/cart 对 这里 其他js压缩过
刷新 单步f10 f8走完
scope ->local
var 变量时;没有初始化 nan undefined
动态改变->icon数量
cart-model.js
[
import { Base } from '../../utils/base.js';
class Cart extends Base{
constructor(){
super();
this._storageKeyName='cart';//缓存区分key 其他token
}
/**
* 加入到购物车
* 如果之前没有这样的商品,则直接添加一条新的记录,数量为counts
* 如果有,则只将相应数量+ counts
* @params:item-{obj}商品对象,counts-{int}商品数目,
*/
add(item,counts){
var cartData=this.getCartDataFromLocal();
var isHasInfo=this._isHasThatOne(item.id,cartData);//item.id动态语言 随意添加删除 不|灵活方便
if(isHasInfo.index==-1){
item.counts = counts;//有更新 没有新增
item.selectStatus=true;//设置是否选中状态 业务需要 购物车是否选中状态
cartData.push(item);//不是等 等复制管理同一个 数组添加push
}else{
cartData[isHasInfo.index].counts += counts;
}
// console.log(cartData);
wx.setStorageSync(this._storageKeyName, cartData);//更新缓存
}
//从缓存里读取数据 item counts 有更新 没有新增
//从缓存里读取购物车数据
getCartDataFromLocal(flag){
var res = wx.getStorageSync(this._storageKeyName);
// wx.getStorageSync(key)在个才对
if(!res){
res=[];
}
// console.log(res);
return res;
}
//商品详情里的购物车 显示数量 动态 icon
//计算购物车内商品总数量
getCartTotalCounts(){
var data=this.getCartDataFromLocal();//debug
// console.log(data.counts);
var counts=0;//init赋值 否则nan undefined
//缓存读取 遍历 累加
// console.log(data);
for (let i = 0; i < data.length; i++){
// console.log(data[i].counts)
// console.log(i)
counts += data[i].counts;//debug =nan
// console.log(data.counts)
// debugger;//debug不行
}
// counts+=data.counts;
// console.log(counts);
return counts;
}
/**
* 判断某个商品是否已经被添加到购物车中,并且返回这个商品的
* 数据以及所在数组中的序号
*/
_isHasThatOne(id,arr){//_表示私有方法区分
//没数据 先空 假设完 继续其他 有再来编写 技巧
//判断商品 和数量 数据库 api 查询
//item数组 一系列 item
var item,result={index:-1};
for(let i=0;i{
// console.log(res)
// console.log(cart.getCartTotalCounts());//nan undefined
this.setData({
cartTotalCounts: cart.getCartTotalCounts(),//加载时读取
product:res
})
})
},
bindPickerChange:function(event){
// // console.log(event); detail vlaue 3+1实际
// var count=event.detail.value;
// // console.log(Number(count)) value字符串 转数字+1
// this.setData({
// productCounts: Number(count)+1
// })
var index=event.detail.value;
var selectedCount=this.data.countsArray[index];
this.setData({
productCounts:selectedCount
})
},
onTabsItemTap:function(event){
var index=product.getDataSet(event,'index');
// console.log(index)
this.setData({
currentTabsIndex:index
});
},
onAddingToCartTap: function (event) {
//点击购物车事件 保存到缓存中去
this.addToCart();
},
addToCart:function(){//保存到缓存中去
var tempObj={};//缓存数组to对象
var keys=['id','name','main_img_url','price'];//数据库api返回一部分数据
//商品product 前面_loadData获取 遍历读取
for(var key in this.data.product){
if(keys.indexOf(key)>=0){
tempObj[key]=this.data.product[key];
}
}
// console.log(tempObj,this.data.productCounts);
cart.add(tempObj,this.data.productCounts);
}
})
}
product.wxml
{
{{item}}
{{item.name}}
{{item.detail}}
七天无理由免费退货
}
12-3商品详情页面动态响应用户加入购物车操作
动态 数据 绑定 icon购物车
事件 更新
this._loadData();
代码 显示数量 加购物车页面
css3+js 动画 TODO 未 ....
product.js
{
onAddingToCartTap: function (event) {
//点击购物车事件 保存到缓存中去
this.addToCart();
// this._loadData();//也可以 慢
var counts=this.data.cartTotalCounts+this.data.productCounts; //快
this.setData({
cartTotalCounts:cart.getCartTotalCounts()
});
}
}
12-4购物车页面开发一
购物车 页面 开发
pages/
选择 全选 减数量 同步价格 删除购物车
购物车 数据 缓存读取
product保存了数据在缓存
js和html交换
model.js和数据交互
onShow里写 显示 执行多次
onLoad 加载 不关闭只执行一次
onReady 准备显扬
记得多算多少
多次加载数据不好 api请求 实时 服务器压力大
选择 区分
cart-model.js
getCartTotalCounts 重构优化
let multiple=100;//变量
//multiple避免 0.05 + 0.01 = 0.060 000 000 000 000 005 的问题,乘以 100 *100
js 浮点数的乘除的误差
cart.js
{
// pages/cart/cart.js
import {Cart} from 'cart-model.js';
var cart=new Cart();//实例化 购物车
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
* 不关闭一次
*/
onLoad: function (options) {
},
/**
* 生命周期函数--监听页面显示
* 多次
*/
onShow: function () {
var cartData = cart.getCartDataFromLocal();//参数选择 区分 缓存读取数据全部
var countsInfo = cart.getCartTotalCounts(true);//参数选择 区分选择总数
this.setData({
selectedCounts:countsInfo,
cartData:cartData
});
},
_calcTotalAccountAndCounts:function(data){
//订单总金额 私有方法
//缓存读取 cartData里面
var len=data.length,
//所需要计算的总价格,但是要注意排除掉未选中的商品
account=0,
//购买商品的总个数
selectedCounts=0,
//购买商品种类的总数 类型里的多少
selectedTypeCounts=0;
let multiple = 100;//变量js 浮点数的乘除的误差
for(let i=0;i 名字 价格 加减 del
名字 价格
加减 del
+ 数量 -
del
全选择 部分
全选图片 文字 数量
下单 总金额 ->icon(俩图片!|选中)
代码 拆解开
布局
同上 过 简单
赶时
静态初始化工作 用户交互事件等...
先静态的 再动态(绑定事件)
前端wxml 后端js 分离
cart.js
{
// pages/cart/cart.js
import {Cart} from 'cart-model.js';
var cart=new Cart();//实例化 购物车
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
* 不关闭一次
*/
onLoad: function (options) {
},
/**
* 生命周期函数--监听页面显示
* 多次
*/
onShow: function () {
var cartData = cart.getCartDataFromLocal();//参数选择 区分 缓存读取数据全部
// var countsInfo = cart.getCartTotalCounts(true);//参数选择 区分选择总数 公共 _calc私有 其他调用 紧当前页用
var cal=this._calcTotalAccountAndCounts(cartData);
this.setData({
// selectedCounts:countsInfo,全部类型的总数目
selectedCounts: cal.selectedCounts,//全部类型的总数目
selectedTypeCounts:cal.selectedTypeCounts,//个体类型的总数
account:cal.account,//总金额
cartData:cartData //全数据
});
},
_calcTotalAccountAndCounts:function(data){
//订单总金额 私有方法
//缓存读取 cartData里面
var len=data.length,
//所需要计算的总价格,但是要注意排除掉未选中的商品
account=0,
//购买商品的总个数
selectedCounts=0,
//购买商品种类的总数 类型里的多少
selectedTypeCounts=0;
let multiple = 100;//变量js 浮点数的乘除的误差
for(let i=0;i
{{item.name}}
${{item.price}}
-
{{item.counts}}
+
x
你还没有添加任何商品
}
选中 和 未
俩个图片 区别 技巧
12-6购物车页面开发三自定义CheckBox及状态控制
购物车订单
单选择2img-!选择
全选 同步 单选
全选择 或 取消
单项 选择 或 取消
全选取消 一个单项取消
全部单项选择 或 取消 全选选择起来
全选 数量 改变 | 总金额改变
事件 业务逻辑
不用微信提供的按钮 选择
自定义图片
不难 有点绕
onTap 点击事件
缓存数据改变 | 数据库数据改变
单项事件 id号传 选中状态
自己想 错误 不够好
{
// console.log(id,!status);
this.data.cartData.id=id;
this.data.cartData.selectStatus=(!status);
// console.log(this.data.cartData.selectStatus)
this.setData({
cartData:this.data.cartData
});
}
自己全选思路 cartdata总个数==selectCount选择的个数
{
toggleSelectAll:function(event){
//全选部分
var status = cart.getDataSet(event, 'status');
for(let i=0;i
{{item.name}}
${{item.price}}
-
{{item.counts}}
+
x
你还没有添加任何商品
}
cart.js
{
// pages/cart/cart.js
import {Cart} from 'cart-model.js';
var cart=new Cart();//实例化 购物车
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
* 不关闭一次
*/
onLoad: function (options) {
},
/**
* 生命周期函数--监听页面显示
* 多次
*/
onShow: function () {
var cartData = cart.getCartDataFromLocal();//参数选择 区分 缓存读取数据全部
// var countsInfo = cart.getCartTotalCounts(true);//参数选择 区分选择总数 公共 _calc私有 其他调用 紧当前页用
var cal=this._calcTotalAccountAndCounts(cartData);
this.setData({
// selectedCounts:countsInfo,全部类型的总数目
selectedCounts: cal.selectedCounts,//全部类型的总数目
selectedTypeCounts:cal.selectedTypeCounts,//个体类型的总数
account:cal.account,//总金额
cartData:cartData //全数据
});
},
_calcTotalAccountAndCounts:function(data){
//订单总金额 私有方法
//缓存读取 cartData里面
var len=data.length,
//所需要计算的总价格,但是要注意排除掉未选中的商品
account=0,
//购买商品的总个数
selectedCounts=0,
//购买商品种类的总数 类型里的多少
selectedTypeCounts=0;
let multiple = 100;//变量js 浮点数的乘除的误差
for(let i=0;i
}
12-8购物车页面开发五
-商品 设置
自己
{
changeCounts:function(event){
var id=cart.getDataSet(event,'id');
var type=cart.getDataSet(event,'type');
// console.log(id,type);
if(type=='cut'){
for(let i=0;this.data.cartData.length;i++){
// console.log(this.data.cartData[i].id==id);
if(this.data.cartData[i].id==id){
// var d=
this.data.cartData[i].counts -=1;
// console.log(d);
}
}
}
this._resetCartData();
}
}
缺少 更新本地缓存
wx.setStorageSync('cart', this.data.cartData);
execSetStorageSync()
-+同一个事件
操作数据 不安全
数量不存在呢
私有方法 提供公共方法调用
写在model 操作
js
{
// pages/cart/cart.js
import {Cart} from 'cart-model.js';
var cart=new Cart();//实例化 购物车
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
* 不关闭一次
*/
onLoad: function (options) {
},
/**
* 生命周期函数--监听页面显示
* 多次
*/
onShow: function () {
var cartData = cart.getCartDataFromLocal();//参数选择 区分 缓存读取数据全部
// var countsInfo = cart.getCartTotalCounts(true);//参数选择 区分选择总数 公共 _calc私有 其他调用 紧当前页用
var cal=this._calcTotalAccountAndCounts(cartData);
this.setData({
// selectedCounts:countsInfo,全部类型的总数目
selectedCounts: cal.selectedCounts,//全部类型的总数目
selectedTypeCounts:cal.selectedTypeCounts,//个体类型的总数
account:cal.account,//总金额
cartData:cartData //全数据
});
},
_calcTotalAccountAndCounts:function(data){
//订单总金额 私有方法
//缓存读取 cartData里面
var len=data.length,
//所需要计算的总价格,但是要注意排除掉未选中的商品
account=0,
//购买商品的总个数
selectedCounts=0,
//购买商品种类的总数 类型里的多少
selectedTypeCounts=0;
let multiple = 100;//变量js 浮点数的乘除的误差
for(let i=0;ifn-fn函数里面都可以调用 错误
var counts = 1;//fn->fn-fn函数里面都可以调用 错误
if(type=='add'){
cart.addCounts(id);//只是更改缓存 蛋页面还没
}
else{
counts=-1;
cart.cutCounts(id)
}
//页面
if (!(this.data.cartData[index].counts == -1)){
this.data.cartData[index].counts += counts;
}
this._resetCartData();
}
})
}
model.js
{
import { Base } from '../../utils/base.js';
class Cart extends Base{
constructor(){
super();
this._storageKeyName='cart';//缓存区分key 其他token
}
/**
* 加入到购物车
* 如果之前没有这样的商品,则直接添加一条新的记录,数量为counts
* 如果有,则只将相应数量+ counts
* @params:item-{obj}商品对象,counts-{int}商品数目,
*/
add(item,counts){
var cartData=this.getCartDataFromLocal();
var isHasInfo=this._isHasThatOne(item.id,cartData);//item.id动态语言 随意添加删除 不|灵活方便
if(isHasInfo.index==-1){
item.counts = counts;//有更新 没有新增
item.selectStatus=true;//设置是否选中状态 业务需要 购物车是否选中状态
cartData.push(item);//不是等 等复制管理同一个 数组添加push
}else{
cartData[isHasInfo.index].counts += counts;
}
// console.log(cartData);
wx.setStorageSync(this._storageKeyName, cartData);//更新缓存
}
//从缓存里读取数据 item counts 有更新 没有新增
//从缓存里读取购物车数据
getCartDataFromLocal(flag){
var res = wx.getStorageSync(this._storageKeyName);
// wx.getStorageSync(key)在个才对
if(!res){
res=[];
}
// console.log(res);
return res;
}
//商品详情里的购物车 显示数量 动态 icon
//计算购物车内商品总数量
//flag true 考虑商品选择状态
getCartTotalCounts(flag){
var data=this.getCartDataFromLocal();//debug
// console.log(data.counts);
var counts=0;//init赋值 否则nan undefined
//缓存读取 遍历 累加
// console.log(data);
for (let i = 0; i < data.length; i++){
if(flag){
if(data[i].selectStatus){
counts += data[i].counts;//debug =nan
}
}
else{
counts += data[i].counts;//debug =nan
}
}
// counts+=data.counts;
// console.log(counts);
return counts;
}
/**
* 判断某个商品是否已经被添加到购物车中,并且返回这个商品的
* 数据以及所在数组中的序号
*/
_isHasThatOne(id,arr){//_表示私有方法区分
//没数据 先空 假设完 继续其他 有再来编写 技巧
//判断商品 和数量 数据库 api 查询
//item数组 一系列 item
var item,result={index:-1};
for(let i=0;i1){
cartData[hasInfo.index].counts+=counts;
}
}
// console.log(cartData)
// this.execSetStorageSync(cartData);//更新本地缓存 封装 暂时不
wx.setStorageSync(this._storageKeyName,cartData);
}
// 增加商品数目
addCounts(id){
this._changeCounts(id,1);
}
//购物车减少商品数目
cutCounts(id){
this._changeCounts(id,-1);
}
}
export {Cart}
}
html
{
{{item.name}}
${{item.price}}
-
{{item.counts}}
+
x
你还没有添加任何商品
}
12-9购物车页面开发六
减少到-1
在wxml 禁用点击
class="btns {{item.counts==1?'disabled':''}}"
model.js
if(hasInfo.data.counts>=1)
改 加减 都 缓存
配 仅 减少 缓存
12-10购物车页面开发七
购物车 删除订单
delete
简单 diyPs
{
delete:function(event){
var id = cart.getDataSet(event, 'id');
var index = this._getProductIndexById(id)//对应商品的下标
this.data.cartData.splice(index,1);
wx.setStorageSync('cart', this.data.cartData);
this._resetCartData();
}
}
删除数据 缓存 在model
delete(ids)
通用 删除多 ids数组
js 数组清空
arr.length=0;
arr.splice(0)
arr = []
清空
Clean
{
delAll:function(){
//数组清空//arr=[];arr.length=0;arr.splice(0) 简单
// this.data.cartData=[];
// wx.setStorageSync('cart', this.data.cartData);
// this._resetCartData();
var len = this.data.cartData.length;
var ids=[];
for(let i=0;i
{{item.name}}
${{item.price}}
-
{{item.counts}}
+
x
你还没有添加任何商品
}
js
{
// pages/cart/cart.js
import {Cart} from 'cart-model.js';
var cart=new Cart();//实例化 购物车
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
* 不关闭一次
*/
onLoad: function (options) {
},
/**
* 生命周期函数--监听页面显示
* 多次
*/
onShow: function () {
var cartData = cart.getCartDataFromLocal();//参数选择 区分 缓存读取数据全部
// var countsInfo = cart.getCartTotalCounts(true);//参数选择 区分选择总数 公共 _calc私有 其他调用 紧当前页用
var cal=this._calcTotalAccountAndCounts(cartData);
this.setData({
// selectedCounts:countsInfo,全部类型的总数目
selectedCounts: cal.selectedCounts,//全部类型的总数目
selectedTypeCounts:cal.selectedTypeCounts,//个体类型的总数
account:cal.account,//总金额
cartData:cartData //全数据
});
},
_calcTotalAccountAndCounts:function(data){
//订单总金额 私有方法
//缓存读取 cartData里面
var len=data.length,
//所需要计算的总价格,但是要注意排除掉未选中的商品
account=0,
//购买商品的总个数
selectedCounts=0,
//购买商品种类的总数 类型里的多少
selectedTypeCounts=0;
let multiple = 100;//变量js 浮点数的乘除的误差
for(let i=0;ifn-fn函数里面都可以调用 错误
var counts = 1;//fn->fn-fn函数里面都可以调用 错误
if(type=='add'){
cart.addCounts(id);//只是更改缓存 蛋页面还没
}
else{
counts=-1;
cart.cutCounts(id)
}
//页面显示减少到-1在wxml 禁用点击
this.data.cartData[index].counts += counts;
// if (!(this.data.cartData[index].counts == -1)){
// }
this._resetCartData();
},
delete:function(event){
var id = cart.getDataSet(event, 'id');
// console.log(id);
var index = this._getProductIndexById(id)//对应商品的下标
this.data.cartData.splice(index,1);//删除某一项商品
// wx.setStorageSync('cart', this.data.cartData);
cart.delete(id);
this._resetCartData();
},
delAll:function(){
//数组清空//arr=[];arr.length=0;arr.splice(0) 简单
// this.data.cartData=[];
// wx.setStorageSync('cart', this.data.cartData);
// this._resetCartData();
var len = this.data.cartData.length;
var ids=[];
for(let i=0;i=1){
cartData[hasInfo.index].counts+=counts;
}
}
// console.log(cartData)
// this.execSetStorageSync(cartData);//更新本地缓存 封装 暂时不
wx.setStorageSync(this._storageKeyName,cartData);
}
// 增加商品数目
addCounts(id){
this._changeCounts(id,1);
}
//购物车减少商品数目
cutCounts(id){
this._changeCounts(id,-1);
}
delete(ids){
//删除数据 缓存 在model 通用 删除多 ids数组
if(!(ids instanceof Array)){
//是否数组 不是 变 单个数组
ids=[ids];
}
var cartData=this.getCartDataFromLocal();//缓存获得商品详情
for(let i=0;iselectDel
js
{
selectDel:function(){
//选中统一删除
var len = this.data.cartData.length;
var ids=[];
for (var i = 0; i 不讲 否则源码 改变 俩个地方
js{
onHide:function(){
//用户离开时保存状态 缓存 单不要直接在js写 model操作数据
// wx.setStorageSync('cart', this.data.cartData);
cart.execSetStorageSync(this.data.cartData);
},
}
model.js
{
execSetStorageSync(data){
//保存缓存
wx.setStorageSync(this._storageKeyName, data);
}
}
D:\PanDownload\【微信小程序】\微信小程序商城构建全栈应用\
第13章 实现微信支付、购买商品流程与用户历史订单+
13-1订单详情页面
netpic
{{选中多少}}
wx.showloading 标签加载状态loading="true"
https://www.cnblogs.com/e0yu/p/8489408.html
wx.showToast
wx.showModel
https://www.cnblogs.com/xqxacm/p/9837218.html
在里面 中间
改 this
改公共变量 创建
配套
点击下单 -> bindtap="submitOrder"
到 订单详情
客户端本身的 点击付钱才和服务器交互
submitOrder:function(event){
//点击下单到 订单详情
wx.navigateTo({
//提交总金额 和 是哪里提交的 区分其他
url: '../order/order?account='+this.data.account+'&from=cart',
})
}
购物车 我的历史订单
cart-model 可以或的缓存 但是全部 没有挑选过的
重写cart-model getCartDataFromLocal()
加标志位
重构
{
getCartDataFromLocal(flag){
var res = wx.getStorageSync(this._storageKeyName);
// wx.getStorageSync(key)在个才对
if(!res){
res=[];
}
//在下单的时候过滤不下单的商品
if (flag){
var newRes=[];
for(let i=0;i
下单时间:
{{basicInfo.orderTime}}
订单编号:
{{basicInfo.orderNo}}
待付款
已付款
已发货
{{addressInfo.name}}
{{addressInfo.mobile}}
{{addressInfo.totalDetail}}
+
添加地址
{{item.name}}
¥{{item.price}}
×{{item.counts}}
}
js
{
onLoad: function (options) {
var productsArr = cart.getCartDataFromLocal(true);//从缓存读取商品 model.js
this.data.account=options.account;
// this.data.productsArr=productsArr;页面不同步 setData才同步数据绑定
this.setData({
productsArr:productsArr,
account:options.account,
orderStatus:0 //是否下单
});
},
}
布局
订单详情
订单编号和下单时间,如果是旧订单就显示
地址
列表 自己再点击删除 保留最少一个 最好不 传递总金额等 多改变
结算
13-2添加用户收货地址
订单详情 地址 方便
提示 也行
还可以修改地址
我的 地址管理
同一个
订单详情
确认 地址 商品
地址 order.wxml
{
{{addressInfo.name}}
{{addressInfo.mobile}}
{{addressInfo.totalDetail}}
+
添加地址
}
有地址不添加 显示地址
没有添加
自写 收货地址 复杂 ->不讲 过 以后有空到时现不理
自己写地址是有 address.wxml 页面
微信提供 收货地址 讲过
微信 model在 utils/address.js
微信写的地址 微信自动地图定位地址...手改
原生组件
https://developers.weixin.qq.com/miniprogram/dev/api/open-api/address/wx.chooseAddress.html
wx.chooseAddress
{
editAddress:function(event){
wx.chooseAddress({
success:function(res){
}
})
},
}
{
属性 类型 说明
userName string 收货人姓名
postalCode string 邮编
provinceName string 国标收货地址第一级地址
cityName string 国标收货地址第二级地址
countyName string 国标收货地址第三级地址
detailInfo string 详细收货地址信息
nationalCode string 收货地址国家码
telNumber string 收货人手机号码
errMsg string 错误信息
示例代码
}
视频音乐声大小 其他->排
版本低不能 兼容处理
成本高
低 自己
高 api
模拟一个地址
新版多个选择提示 申请权限
真机 跳出 原生管理界面
真机测试 域名 本地代理对外开发
否则自己配域名 服务器 dns...
拼合地址
在model.js弄
utils/address
{
import {Base} from 'base.js';
import {Config} from 'config.js';
class Address extends Base{
constructor(){
super();
}
setAddressInfo(res){
//res来源wx | 数据库的地址 写兼容代码
var province=res.provinceName||res.province;
var city=res.cityName||res.city;
var country=res.countyName||res.country;//country
var detail=res.detailInfo||res.detail;
// console.log(res)
// console.log(province,city,country,detail);
//拼合
var totalDetail=city+country+detail;
if(!this.isCenterCity(province)){
totalDetail = province + totalDetail;//直辖市
}
return totalDetail;
}
/**
* 是否为直辖市
*/
isCenterCity(name){
var centerCitys=['北京市','天津市','上海市','重庆市'];
var flag=centerCitys.indexOf(name)>=0;
return flag;//bool
}
}
export {Address};
}
order.js
[
// pages/order/order.js
import {Cart} from '../cart/cart-model.js';
import {Order} from 'order-model.js';
import {Address} from '../../utils/address.js';
var cart=new Cart();
var order=new Order();
var address=new Address();
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
var productsArr = cart.getCartDataFromLocal(true);//从缓存读取商品 model.js
this.data.account=options.account;
// this.data.productsArr=productsArr;页面不同步 setData才同步数据绑定
this.setData({
productsArr:productsArr,
account:options.account,
orderStatus:0 //是否下单
});
},
editAddress:function(event){
//获得地址 编辑地址 wxapi组件
var that=this;//回调函数this改变
wx.chooseAddress({
success:function(res){
// console.log(address.setAddressInfo(res))
var addressInfo={
name:res.userName,
mobile:res.telNumber,
totalDetail:address.setAddressInfo(res) //../../utils/address.js model拼合
};
that._bindAddressInfo(addressInfo);
}
})
},
/**
* 绑定地址信息
*/
_bindAddressInfo:function(addressInfo){
// console.log(addressInfo);
this.setData({
addressInfo:addressInfo
});
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
},
/**
* 生命周期函数--监听页面显示
*/
onShow: function () {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide: function () {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload: function () {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage: function () {
}
})
]
13-3保存用户收货地址
刷新无
保存用户收货地址
微信保存地址
数据库保存地址
小程序缓存地址
//暂时不行保存 要携带token
this.request(param);//请求服务器api 更改
控制台 NetWork
name address 点击
右边 preview 显示结果 显示
error_code: 10001
msg: "Token已过期或无效Token"
request_url: "/api/v1/address"
自己写保存令牌
utils/
address.js
{
import {Base} from 'base.js';
import {Config} from 'config.js';
class Address extends Base{
constructor(){
super();
}
setAddressInfo(res){
//res来源wx | 数据库的地址 写兼容代码
var province=res.provinceName||res.province;
var city=res.cityName||res.city;
var country=res.countyName||res.country;//country
var detail=res.detailInfo||res.detail;
// console.log(res)
// console.log(province,city,country,detail);
//拼合
var totalDetail=city+country+detail;
if(!this.isCenterCity(province)){
totalDetail = province + totalDetail;//直辖市
}
return totalDetail;
}
/**
* 是否为直辖市
*/
isCenterCity(name){
var centerCitys=['北京市','天津市','上海市','重庆市'];
var flag=centerCitys.indexOf(name)>=0;
return flag;//bool
}
//更新保存地址
submitAddress(data,callBack){
data=this._setUpAddress(data);
var param={
url:'address',
type:'post',
data:data,
sCallBack:function(res){
callBack&&callBack(true,res);
},eCallBack(res){
callBack&&callBack(false,res);
}
};
//暂时不行保存 要携带token
this.request(param);//请求服务器api 更改
}
//保存地址 微信的地址保存 换 服务器数据库格式的地址
_setUpAddress(res){
var formData = {
name:res.userName,
province:res.provinceName,
city:res.cityName,
country:res.countyName,
mobile:res.telNumber,
detail:res.detailInfo
};
return formData;
}
}
export {Address};
}
order.js
{
// pages/order/order.js
import {Cart} from '../cart/cart-model.js';
import {Order} from 'order-model.js';
import {Address} from '../../utils/address.js';
var cart=new Cart();
var order=new Order();
var address=new Address();
Page({
/**
* 页面的初始数据
*/
data: {
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
var productsArr = cart.getCartDataFromLocal(true);//从缓存读取商品 model.js
this.data.account=options.account;
// this.data.productsArr=productsArr;页面不同步 setData才同步数据绑定
this.setData({
productsArr:productsArr,
account:options.account,
orderStatus:0 //是否下单
});
},
editAddress:function(event){
//获得地址 编辑地址 wxapi组件
var that=this;//回调函数this改变
wx.chooseAddress({
success:function(res){
// console.log(address.setAddressInfo(res))
var addressInfo={
name:res.userName,
mobile:res.telNumber,
totalDetail:address.setAddressInfo(res) //../../utils/address.js model拼合
};
that._bindAddressInfo(addressInfo);
//保存地址
address.submitAddress(res,(flag)=>{
if(!flag){
that.showTips('操作提示','地址信息更新失败!');
}
})
}
})
},
/**
* 提示窗口
* params:title-{string}标题
* content-{string}内容
* flag-{bool}是否跳转到"我的页面"
*/
showTips:function(title,content,flag){
wx.showModal({
title: title,
content: content,
showCancel:false,//取消按钮hidden
success:function(res){
if(flag){
wx.switchTab({
url: '../my/my',// /pages/my/my
});
}
}
})
},
/**
* 绑定地址信息
*/
_bindAddressInfo:function(addressInfo){
// console.log(addressInfo);
this.setData({
addressInfo:addressInfo
});
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady: function () {
},
/**
* 生命周期函数--监听页面显示
*/
onShow: function () {
},
/**
* 生命周期函数--监听页面隐藏
*/
onHide: function () {
},
/**
* 生命周期函数--监听页面卸载
*/
onUnload: function () {
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
},
/**
* 用户点击右上角分享
*/
onShareAppMessage: function () {
}
})
}
code 不是在 url传递 get才是
http://z.cn/api/v1/token/user?code='+res.code
post 是在 data
而token 是headers
申请令牌
token:function(){
wx.login({
success:function(res){
var code=res.code;
wx.request({
url: 'http://z.cn/api/v1/token/user',//get 在url
data: {
code: code//post data
},
method: 'POST',
success:function(res){
wx.setStorageSync('token', res.data.token);
}
})
}
})
},
mobile:res.telNumber,//模拟的电话不符合服务器
mobile:'13533347986',//res.telNumber模拟的电话不符合服务器
address.js 临时替换
手机 保存 请求
submitAddress(data,callBack)
data.mobile = '13533347986';//模拟的电话不符合服务器 临时替换 可del
请求配套+更新服务器地址diy
地址 保存到服务器了
但是 页面 没有保存
没有请求服务器 address地址
地址未知 下订单 快照等 复杂过 配套当时
没有刷新 显示
//请求服务器address地址
// var param={
// //地址未知 下订单 快照等 复杂过 配套当时
// };
// address.request(param);
13-4客户端令牌管理机制(重要)
图片
API --->如果401,重新请求令牌>---Api
^如果令牌失效或者不存在,则请求Token
小程序 -初始化时--> 检测令牌 ^携带令牌访问API
| V model
V<----读取令牌---Stroage------>读取令牌
考虑问题:如果检测时有效,访问时无效怎么办
停留一段时间
重新 自动 获得令牌
13-5新增API令牌验证接口
utils/token.js 类 模型
统一操作令牌
服务端编程思维 测试 应用于 前端
服务器 api
还没编写 检验 令牌 接口
v1/token
{
//校验Token 有效 无效
public function verifyToken($token=''){
//简单 不写验证器 可写
if (!$token){
throw new ParameterException([
'token不允许为空'
]);
}
$valid=\app\api\service\Token::verifyToken($token);
return [
'isValid'=>$valid
];
}
}
route
Route::post('api/:version/token/verify','api/:version.Token/verifyToken');
Service/token
{
//校验令牌
public static function verifyToken($token){
$exist=Cache::get($token);//判断令牌是否在服务器缓存
if ($exist){
return true;
}else{
return false;
}
}
}
utils/token.js
{
import { Config } from 'config.js';
class Token{
constructor(){
}
verify(){
}
}
}
13-6客户端Token类实现
base.js 是模型的基类
不要继承
token.js 工具类 父级关系不太好
提交订单时
令牌 才在header 头上传递
utils/
token.js
{
import { Config } from 'config.js';
class Token{
constructor(){
//成员变量
this.verifyUrl=Config.restUrl+'token/verity';//校验令牌存在服务器
this.tokenUrl=Config.restUrl+'token/user';//申请令牌
}
//校验令牌存在 存在判断合法过期有效 无申请
verify(){
var token=wx.getStorageSync('token');
if(!token){
this.getTokenFromServer();//无申请token
}
else{
this._verifyFromServer(token);//存在判断合法过期有效
}
}
//从服务器获取token
getTokenFromServer(callBack){
//申请 token
var that=this;
wx.login({
success:function(res){
wx.request({
url: that.tokenUrl,
method:'POST',
data:{
code:res.code
},
success:function(res){
wx.setStorageSync('token', res.data.token);
callBack&&callBack(res.data.token);
}
})
}
})
}
//携带令牌去服务器校验令牌
_verifyFromServer(token){
var that=this;
wx.request({
url: that.verifyUrl,
method:'POST',
data:{
token:token
},
success:function(res){
var valid=res.data.isValid;
if(!valid){
that.getTokenFromServer();//服务器没有对应的token 不合法 重新申请
}
}
})
}
}
export{Token};
}
13-7重构Base下的Reques方法
小程序
app.js
App({
//自己的生命周期函数
onLaunch onLaunch(全局只触发一次)
onShow后台进入前台显示,会触发
onHide当小程序从前台进入后台,会触发
onError 当小程序发生脚本错误,或者 api 调用失败时,会触发 onError 并带上错误信息
})
微信已经检查userpasswd
小程序不用
但令牌 过期没有 要自己申请 对用户透明的
重构Base
判断 服务器返回的状态码
服务器 失败 成功 都返回
小程序 只是success接受返回结果 并不知道 是否成功
app.js
{
import {Token} from 'utils/token.js';
App({
/**
* 当小程序初始化完成时,会触发 onLaunch(全局只触发一次)
*/
onLaunch: function () {
var token=new Token();
token.verify();
},
/**
* 当小程序启动,或从后台进入前台显示,会触发 onShow
*/
onShow: function (options) {
},
/**
* 当小程序从前台进入后台,会触发 onHide
*/
onHide: function () {
},
/**
* 当小程序发生脚本错误,或者 api 调用失败时,会触发 onError 并带上错误信息
*/
onError: function (msg) {
}
})
}
=>箭头函数保持 this环境变量
this
x:function(){} 回调函数 this环境改变
用到写 不用不学 新未必是好的
utils/base.js
{
import {Config} from '../utils/config.js'
import {Token} from '../utils/token.js';
class Base{
constructor(){
// this.baseRequestUrl="http://z.cn/api/v1/"
//静态调用
this.baseRequestUrl=Config.restUrl;
}
request(params){//params数组对象
var that=this;
var url = this.baseRequestUrl + params.url;
if(!params.type){
params.type="GET";
}
wx.request({
url: url,
data:params.data,
method:params.type,
header:{
'content-type':'application/json',
'token':wx.getStorageSync('token')
},//Token
success:function(res){
// if(params.sCallBack){
// params.sCallBack(res);
// }
var code=res.statusCode.toString();
var startChar=code.charAt(0);//获得第一个字符 20x 40x ...状态码 第一个够判断成功
//判断 服务器返回的状态码
if(startChar=='2'){
params.sCallBack&¶ms.sCallBack(res.data);
}
else{
if(code=='401'){
//比如token问题
//重新获得token token.getTokenFromServer
//重新请求 wx.request | base.request
that._refetch(params);
}
params.eCallBack&¶ms.eCallBack(res.data);
}
},
fail:function(err){
console.log(err);//不到service 网络中断...
}
})
}
_refetch(params){
var token=new Token();
token.getTokenFromServer((token)=>{
this.request(params);
});
}
/*获得元素上的绑定的值 */
getDataSet(event,key){
return event.currentTarget.dataset[key];
}
}
export{Base}
}
13-8解决无限未授权重试的问题
返回多次调用 base.request
_refetch
概率低 预防
if(code=='401'){
//比如token问题
//重新获得token token.getTokenFromServer
//重新请求 wx.request | base.request
that._refetch(params);
}
多次401
循环 死机...
不要麻烦 就多些 request2
但代码重复 --.一个好
utils/base
{
import {Config} from '../utils/config.js'
import {Token} from '../utils/token.js';
class Base{
constructor(){
// this.baseRequestUrl="http://z.cn/api/v1/"
//静态调用
this.baseRequestUrl=Config.restUrl;
}
//当noRefetch为true时,不做未授权重试机制 不传!false执行
//做计数器 多次不执行也行
request(params,noRefetch){//params数组对象
var that=this;
var url = this.baseRequestUrl + params.url;
if(!params.type){
params.type="GET";
}
wx.request({
url: url,
data:params.data,
method:params.type,
header:{
'content-type':'application/json',
'token':wx.getStorageSync('token')
},//Token
success:function(res){
// if(params.sCallBack){
// params.sCallBack(res);
// }
var code=res.statusCode.toString();
var startChar=code.charAt(0);//获得第一个字符 20x 40x ...状态码 第一个够判断成功
//判断 服务器返回的状态码
if(startChar=='2'){
params.sCallBack&¶ms.sCallBack(res.data);
}
else{
//AOP思想
if(code=='401'){
//比如token问题
//重新获得token token.getTokenFromServer
//重新请求 wx.request | base.request
if(!noRefetch){ //死循环 多次401
that._refetch(params);
}
}
params.eCallBack&¶ms.eCallBack(res.data);
}
},
fail:function(err){
console.log(err);//不到service 网络中断...
}
})
}
_refetch(params){
var token=new Token();
token.getTokenFromServer((token)=>{
this.request(params,true);
});
}
/*获得元素上的绑定的值 */
getDataSet(event,key){
return event.currentTarget.dataset[key];
}
}
export{Base}
}
13-9测试未授权重试机制
以前模拟 电话是手机
现在是座机 临时赋值 调试
NetWork
小圆点 红开 记录
服务器 缓存清除
测试 令牌失效 |小程序 删除
runtime/cache 删除
操作提示 是二次才成功
第一次 还是eCallBack
解决:
也noRefetch
数据库 user_address 更新了
重复机制 重要
utils/base.js
{
import {Config} from '../utils/config.js'
import {Token} from '../utils/token.js';
class Base{
constructor(){
// this.baseRequestUrl="http://z.cn/api/v1/"
//静态调用
this.baseRequestUrl=Config.restUrl;
}
//当noRefetch为true时,不做未授权重试机制 不传!false执行
//做计数器 多次不执行也行
request(params,noRefetch){//params数组对象
var that=this;
var url = this.baseRequestUrl + params.url;
if(!params.type){
params.type="GET";
}
wx.request({
url: url,
data:params.data,
method:params.type,
header:{
'content-type':'application/json',
'token':wx.getStorageSync('token')
},//Token
success:function(res){
// if(params.sCallBack){
// params.sCallBack(res);
// }
var code=res.statusCode.toString();
var startChar=code.charAt(0);//获得第一个字符 20x 40x ...状态码 第一个够判断成功
//判断 服务器返回的状态码
if(startChar=='2'){
params.sCallBack&¶ms.sCallBack(res.data);
}
else{
//AOP思想
if(code=='401'){
//比如token问题
//重新获得token token.getTokenFromServer
//重新请求 wx.request | base.request
if(!noRefetch){ //死循环 多次401
that._refetch(params);
}
}
if(noRefetch){ //不在重新请求时
params.eCallBack && params.eCallBack(res.data);
}
}
},
fail:function(err){
console.log(err);//不到service 网络中断...
}
})
}
_refetch(params){
var token=new Token();
token.getTokenFromServer((token)=>{
this.request(params,true);
});
}
/*获得元素上的绑定的值 */
getDataSet(event,key){
return event.currentTarget.dataset[key];
}
}
export{Base}
}
13-10在Order页面加载和显示用户地址
order 页面 没显示地址
没有onload请求 地址 显示
在utils/address.js 模型类里 写
获得 地址
address.js
{
import {Base} from 'base.js';
import {Config} from 'config.js';
class Address extends Base{
constructor(){
super();
}
setAddressInfo(res){
//res来源wx | 数据库的地址 写兼容代码
var province=res.provinceName||res.province;
var city=res.cityName||res.city;
var country=res.countyName||res.country;//country
var detail=res.detailInfo||res.detail;
// console.log(res)
// console.log(province,city,country,detail);
//拼合
var totalDetail=city+country+detail;
if(!this.isCenterCity(province)){
totalDetail = province + totalDetail;//直辖市
}
return totalDetail;
}
/**
* 是否为直辖市
*/
isCenterCity(name){
var centerCitys=['北京市','天津市','上海市','重庆市'];
var flag=centerCitys.indexOf(name)>=0;
return flag;//bool
}
//更新保存地址
submitAddress(data,callBack){
data=this._setUpAddress(data);
data.mobile = '13533347986';//模拟的电话不符合服务器 临时替换 可del
// console.log(data);
var param={
url:'address',
type:'post',
data:data,
sCallBack:function(res){
callBack&&callBack(true,res);
},eCallBack(res){
callBack&&callBack(false,res);
}
};
//暂时不行保存 要携带token
this.request(param);//请求服务器api 更改
}
//保存地址 微信的地址保存 换 服务器数据库格式的地址
_setUpAddress(res){
var formData = {
name:res.userName,
province:res.provinceName,
city:res.cityName,
country:res.countyName,
mobile:res.telNumber,
detail:res.detailInfo
};
return formData;
}
//获得我自己的收货地址
getAddress(callBack){
var that=this;
var param={
url:'address',
// method:'post', 可能这 address接口没写
sCallBack:function(res){
if(res){
res.totalDetail=that.setAddressInfo(res);//获得设置的地址
callBack&&callBack(res);//将地址返回调用出
}
}
};
this.request(param);
}
}
export {Address};
}
order.js
{
onLoad()
//显示收货地址
address.getAddress((res)=>{
this._bindAddressInfo(res);//重新数据绑定
});
}
可能 address 一个带参数token等等
一个 请求 不带参数
13-11服务器API新增获取用户地址
还数据管理
可能 address 一个带参数token 地址 等等
一个 请求 带token 不带参数 地址
NetWord
name header
request token
服务器
v1/address
{
protected $beforeActionList=[//前置操作
'checkPrimaryScope'=>['only'=>'createorupdateaddress,getuseraddress']//要小写createOrUpdateAddress
];
public function getUserAddress(){
//获得用户地址
$uid=\app\api\service\Token::getCurrentUid();
$userAddress=UserAddress::where('user_id',$uid)->find();
if (!$userAddress){
throw new UserException([
'msg'=>'用户地址不存在',
'errorCode'=>60001
]);
}
return $userAddress;
}
}
route
Route::get('api/:version/address','api/:version.Address/getUserAddress');
get请求 区别
post 保存地址
13-12下单与支付
order.wxml
{
}
分销 ->到时 源码 更 ---->
俩个支付和在一起
历史订单 点击 和 order页一样
发货 收货 快递时间 哪里 api 扩展
订单已生成 删除购物车
支付成功 也是
deleteProducts
测试
{msg: "商品列表参数错误", error_code: 10000, request_url: "/api/v1/order"}
error_code: 10000
msg: "商品列表参数错误"
request_url: "/api/v1/order"
调试
counts
storage
token
cart
newOrder true
NetWork
order
create_time: "2019-12-15 17:44:53"
order_id: "10"
order_no: "CC15030936412702"
pass: true
库存量检测通过
成功创建订单号
pre_order
根据订单号 id 支付 过 没企业
未定义数组索引: prepay_id
prepay_id 没企业
请求 微信支付接口 返回的prepay_id
数据库没 order prepay_id字段
表示 支付了 但没企业 没...
4
出 prepay_id 接口 小程序 成功 过
除非 数据库 模拟 prepay_id
但是
服务器
Service pay.php
private function getPaySignature
//$wxOrder['prepay_id ']='wx201910272009395522657a690389285100';//没企业 临时测试加
微信返回的预支付交易会话标识
小程序调试 + 改数据库 稳定 不改服务器代码
调试
debug 步入 回调 有点麻烦
总结
服务器 请求 微信支付 接口 时出问题
没企业 过
api->base.js->order-model.js-order.js
开始不行->
wxapi-serviceapi
不正常返回
支付成功不行
失败也不行
服务器 模拟返回可以在base==500 模拟
预防其他问题 测试完 改变
小程序 返回结果比较难 改状态 模拟 当无 过
思路
callBack&&callBack(2);
除非 反过来调试
success 也是返回2 成功 或者反过来1
模拟错误的信息肯定返回fail
不用模拟成功失败
order.js _execPay statusCode!=0 都执行
fail=1 success=2 够用 可以不改 不是0就行了
fail api本身就失败 测试失败当成功1变2 测试
_orderFail
测试 先加入购物车 改数据库库存量
对应商品
order.js
{
// pages/order/order.js
import {Cart} from '../cart/cart-model.js';
import {Order} from 'order-model.js';
import {Address} from '../../utils/address.js';
var cart=new Cart();
var order=new Order();
var address=new Address();
Page({
/**
* 页面的初始数据
*/
data: {
id:-1,//订单号id
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
var productsArr = cart.getCartDataFromLocal(true);//从缓存读取商品 model.js
this.data.account=options.account;
// this.data.productsArr=productsArr;页面不同步 setData才同步数据绑定
//请求服务器address地址
// var param={
// //地址未知 下订单 快照等 复杂过 配套当时
// };
// address.request(param);
this.setData({
productsArr:productsArr,
account:options.account,
orderStatus:0 //是否下单
});
//显示收货地址
address.getAddress((res)=>{
this._bindAddressInfo(res);//重新数据绑定
});
},
editAddress:function(event){
//获得地址 编辑地址 wxapi组件
var that=this;//回调函数this改变
wx.chooseAddress({
success:function(res){
// console.log(address.setAddressInfo(res))
var addressInfo={
name:res.userName,
mobile:res.telNumber,//模拟的电话不符合服务器
totalDetail:address.setAddressInfo(res) //../../utils/address.js model拼合
};
that._bindAddressInfo(addressInfo);
//保存地址
address.submitAddress(res,(flag)=>{
if(!flag){
that.showTips('操作提示','地址信息更新失败!');
}
})
}
})
},
/**
* 提示窗口
* params:title-{string}标题
* content-{string}内容
* flag-{bool}是否跳转到"我的页面"
*/
showTips:function(title,content,flag){
wx.showModal({
title: title,
content: content,
showCancel:false,//取消按钮hidden
success:function(res){
if(flag){
wx.switchTab({
url: '../my/my',// /pages/my/my
});
}
}
})
},
/**
* 绑定地址信息
*/
_bindAddressInfo:function(addressInfo){
// console.log(addressInfo);
this.setData({
addressInfo:addressInfo
});
},
//下单和付款
pay:function(){
if(!this.data.addressInfo){
this.showTips('下单提示','请填写您的收货地址');
return;
}
//写在一起 历史订单 点击 和 order页一样
if(this.data.orderStatus==0){
this._firstTimePay();//下单页 支付 服务器订单号还没创建
}
else{
this._oneMoresTimePay();//历史订单 支付 服务器订单号已创建
}
},
//第一次支付 order下单页
_firstTimePay:function(){
var orderInfo=[],
productInfo=this.data.productsArr,
order=new Order();
// console.log(this.data.productsArr)
for(let i=0;i{
//订单生成成功 服务器订单号 成功
console.log('订单生成成功:',data);
if(data.pass){
//更新订单状态 order字段status 1 2 3 4
var id = data.order_id;//服务器返回的订单号id
that.data.id=id; //订单号 id
// that.data.fromCartFlag=false;//下单页
//开始支付 在orderModel
that._execPay(id);
}
else{//data.pass=false 库存量订单不通过
that._orderFail(data);//下单失败 商品id和商品返回
}
});
},
//下单失败 params:data-{obj} 订单结果信息
_orderFail:function(data){
var nameArr=[],
name='',
str='',
pArr=data.pStatusArray;//商品快照信息
for(let i=0;i15){
name=name.substr(0,12)+'...';
}
nameArr.push(name);
if(nameArr.length>=2){
break;
}
}
}
str += nameArr.join('、');
if(nameArr.length>0){
str+=' 等';
}
str+=' 缺货';
wx.showModal({
title: '下单失败',
content:str,
showCancel:false,
success:function(res){}
})
},
//开始支付 params:id-{int} 订单id
_execPay:function(id){
var that=this;
order.execPay(id,(statusCode)=>{
console.log('支付成功状态2成功1取消失败0没支付:',statusCode)
if(statusCode!=0){
//将已经下单的商品从购物车删除 当状态为0时,表示下单支付失败
that.deleteProducts();
var flag=statusCode==2;//=2表示支付成功
wx.navigateTo({//下单成功 挑战 支付成功页面
url: '../pay-result/pay-result?id='+id+'&flag='+flag+'&from=order',
});
}
});
},
//将已经下单的商品从购物车删除
deleteProducts:function(){
var ids=[],arr=this.data.productsArr;
for(let i=0;i{
this.request(params,true);
});
}
/*获得元素上的绑定的值 */
getDataSet(event,key){
return event.currentTarget.dataset[key];
}
}
export{Base}
}
13-13支付后刷新订单详情页面
支付后刷新订单详情页面
pay-result/pay-result
wxml wxss ->
{
支付成功
查看订单
支付失败
查看订单
}
兴趣 精神 防止疲劳ge一会 别zui无意义 自己 错对 改变不了 说行快
福利老
支付 返回订单页面 true false flag
page.js
不能有多个onShow
支付 下单 时 进俩次 onShow
this.data.id 不能为-1 存在进入 第一次不应该进入
null
图片 商品参数 不显示 服务器返回参数改变 到时
源码 视频优化 新旧
测试成功 支付
数据是从服务器加载来的
数据库字段status 支付状态
wx改接受时的status
或 数据库 status字段
视频 还不行
数据库 order status没进行更改
1 2 3 4
自己思路
支付时 成功失败 同时请求服务器更改 支付状态
callBack&&callBack(2);设置
//支付时 成功失败 同时请求服务器更改 支付状态
_toServiceStatus(status){
var params={
// api接口没编写 未知
}
this.request(params);
}
order-model
{
import { Base } from '../../utils/base.js';
class Order extends Base{
constructor(){
super();
this._storageKeyName='newOrder';
}
//下订单
doOrder(param,callBack){
var that=this;
var allParams={
url:'order',
type:'post',
data:{
products:param
},
sCallBack:function(data){
that.execSetStorageSync(true);
callBack&&callBack(data);
},
eCallBack:function(){}
};
this.request(allParams);
}
/**
* 拉起微信支付
* params:
* norderNumber-{int}订单id
* return:
* callback-{obj} 回调方法,返回参数
* 可能值0:商品缺货等原因导致订单不能支付;
* 1:支付失败或者支付取消
* 2:支付成功
*/
execPay(orderNumber,callBack){
var allParams={
url:'pay/pre_order',
type:'post',
data:{
id:orderNumber
},
sCallBack:function(data){
console.log('服务器返回:',data);
var timeStamp=data.timeStamp;//服务器返回的时间戳
if(timeStamp){//服务器返回的订单详细 存在 才支付
wx.requestPayment({//微信支付api
timeStamp: timeStamp.toString(),
nonceStr: data.nonceStr,
package: data.package,
signType: data.signType,
paySign: data.paySign,
success:function(){
callBack&&callBack(2);//更新订单支付状态 2支付成功
},
fail:function(){
callBack && callBack(1);//支付失败或者支付取消
//服务器 模拟返回可以在base==500 模拟
// 预防其他问题 测试完 改变
// 小程序 返回结果比较难 改状态 模拟 当无 过
// 思路
// callBack && callBack(2);
// 除非 反过来调试
// success 也是返回2 成功 或者反过来1
// 不用模拟成功失败
// order.js _execPay statusCode != 0 都执行
// fail = 1 success = 2 够用 可以不改 不是0就行了
// fail api本身就失败 测试失败当成功1变2 测试
}
})
}
else{
callBack && callBack(0);//更新订单支付状态 0没下单没支付
}
}
};
// console.log(allParams);
this.request(allParams);
}
//支付时 成功失败 同时请求服务器更改 支付状态
_toServiceStatus(status){
var params={
// api接口没编写 未知
}
this.request(params);
}
//获得订单的具体内容 服务器数据库中 订单快照
getOrderInfoById(id,callBack){
var that=this;
var allParams={
url:'order/'+id,
sCallBack:function(data){
callBack&&callBack(data);
},
eCallBack:function(){}
};
this.request(allParams);
}
//本地缓存 保存/更新
execSetStorageSync(data){
wx.setStorageSync(this._storageKeyName, data);//true 下单了
}
}
export{Order};
}
order.js
{
// pages/order/order.js
import {Cart} from '../cart/cart-model.js';
import {Order} from 'order-model.js';
import {Address} from '../../utils/address.js';
var cart=new Cart();
var order=new Order();
var address=new Address();
Page({
/**
* 页面的初始数据
*/
data: {
id:null,//订单号id onshow2 -1 null
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
var productsArr = cart.getCartDataFromLocal(true);//从缓存读取商品 model.js
this.data.account=options.account;
// this.data.productsArr=productsArr;页面不同步 setData才同步数据绑定
//请求服务器address地址
// var param={
// //地址未知 下订单 快照等 复杂过 配套当时
// };
// address.request(param);
this.setData({
productsArr:productsArr,
account:options.account,
orderStatus:0 //是否下单
});
//显示收货地址
address.getAddress((res)=>{
this._bindAddressInfo(res);//重新数据绑定
});
},
//下单后, 支付成功或者失败后,点左上角返回时能够更新订单状态
//所以放在onshow中
onShow:function(){
if(this.data.id){//订单号 id存在 debug
var that=this;
var id=this.data.id;
order.getOrderInfoById(id,(data)=>{
that.setData({
orderStatus:data.status,//数据库字段status 支付状态 改2 成功2
productsArr:data.snap_items,
account:data.total_price,
basicInfo:{
orderTime:data.create_time,
orderNo:data.order_no
}
});
//快照地址
var addressInfo=data.snap_address;
addressInfo.totalDetail=address.setAddressInfo(addressInfo);
that._bindAddressInfo(addressInfo);
});
}
},
editAddress:function(event){
//获得地址 编辑地址 wxapi组件
var that=this;//回调函数this改变
wx.chooseAddress({
success:function(res){
// console.log(address.setAddressInfo(res))
var addressInfo={
name:res.userName,
mobile:res.telNumber,//模拟的电话不符合服务器
totalDetail:address.setAddressInfo(res) //../../utils/address.js model拼合
};
that._bindAddressInfo(addressInfo);
//保存地址
address.submitAddress(res,(flag)=>{
if(!flag){
that.showTips('操作提示','地址信息更新失败!');
}
})
}
})
},
/**
* 提示窗口
* params:title-{string}标题
* content-{string}内容
* flag-{bool}是否跳转到"我的页面"
*/
showTips:function(title,content,flag){
wx.showModal({
title: title,
content: content,
showCancel:false,//取消按钮hidden
success:function(res){
if(flag){
wx.switchTab({
url: '../my/my',// /pages/my/my
});
}
}
})
},
/**
* 绑定地址信息
*/
_bindAddressInfo:function(addressInfo){
// console.log(addressInfo);
this.setData({
addressInfo:addressInfo
});
},
//下单和付款
pay:function(){
if(!this.data.addressInfo){
this.showTips('下单提示','请填写您的收货地址');
return;
}
//写在一起 历史订单 点击 和 order页一样
if(this.data.orderStatus==0){
this._firstTimePay();//下单页 支付 服务器订单号还没创建
}
else{
this._oneMoresTimePay();//历史订单 支付 服务器订单号已创建
}
},
//第一次支付 order下单页
_firstTimePay:function(){
var orderInfo=[],
productInfo=this.data.productsArr,
order=new Order();
// console.log(this.data.productsArr)
for(let i=0;i{
//订单生成成功 服务器订单号 成功
console.log('订单生成成功:',data);
if(data.pass){
//更新订单状态 order字段status 1 2 3 4
var id = data.order_id;//服务器返回的订单号id
that.data.id=id; //订单号 id
// that.data.fromCartFlag=false;//下单页
//开始支付 在orderModel
that._execPay(id);
}
else{//data.pass=false 库存量订单不通过
that._orderFail(data);//下单失败 商品id和商品返回
}
});
},
//下单失败 params:data-{obj} 订单结果信息
_orderFail:function(data){
var nameArr=[],
name='',
str='',
pArr=data.pStatusArray;//商品快照信息
for(let i=0;i15){
name=name.substr(0,12)+'...';
}
nameArr.push(name);
if(nameArr.length>=2){
break;
}
}
}
str += nameArr.join('、');
if(nameArr.length>0){
str+=' 等';
}
str+=' 缺货';
wx.showModal({
title: '下单失败',
content:str,
showCancel:false,
success:function(res){}
})
},
//开始支付 params:id-{int} 订单id
_execPay:function(id){
var that=this;
order.execPay(id,(statusCode)=>{
console.log('支付成功状态2成功1取消失败0没支付:',statusCode)
if(statusCode!=0){
//将已经下单的商品从购物车删除 当状态为0时,表示下单支付失败
that.deleteProducts();
var flag=statusCode==2;//=2表示支付成功
wx.navigateTo({//下单成功 挑战 支付成功页面
url: '../pay-result/pay-result?id='+id+'&flag='+flag+'&from=order',
});
}
});
},
//将已经下单的商品从购物车删除
deleteProducts:function(){
var ids=[],arr=this.data.productsArr;
for(let i=0;itrue,//检测通过
'totalCount'=>0,//所有总数
'orderPrice'=>0,//所有商品总价
'pStatusArray'=>[]//商品
];
foreach ($this->oProducts as $oProduct){
// $oPID=$oProduct['id'];
// $oCount=$oProduct['count'];
$pStatus=$this->getProductStatus($oProduct['product_id'],$oProduct['count'],$this->products);
if (!$pStatus['haveStock']){
$status['pass']=false;//又一个订单不通过 false
}
$status['orderPrice']+=$pStatus['totalPrice'];//所有商品总价
$status['totalCount']+=$pStatus['counts'];//所有商品总数
array_push($status['pStatusArray'],$pStatus);//商品全部信息
}
return $status;
}
private function getProductStatus($oPID,$oCount,$products){
//把提交的商品id 数量 对应的库存量
$pIndex=-1;
//$pStatus某个商品 历史订单
$pStatus=[
'id'=>null,//商品id
'haveStock'=>false,//库存量
'counts'=>0,//提交的数目
'price'=>0,//单价
'name'=>'',//商品名
'totalPrice'=>0 ,//单个商品 多少个 总价
'main_img_url'=>null //主图片
];
//小于 sql库存的总和 判断商品存在
for ($i=0;$i'id为:'.$oPID.'的商品不存在,创建订单失败'
]);
}
else{
$product=$products[$pIndex];//对应商品
$pStatus['id']=$product['id'];
$pStatus['name']=$product['name'];//每次调用传一个商品 对应name
$pStatus['counts']=$oCount;
$pStatus['price']=$product['price'];
$pStatus['mian_img_url']=$product['mian_img_url'];
$pStatus['totalPrice']=$product['price']*$oCount;
if ($product['stock']-$oCount>=0){//是否有库存量
$pStatus['haveStock']=true;
}
}
return $pStatus;
}
}
图片地址名写错 注意
微信小程序调试 appdata
重新下 刚没保存 快照
在测试 下单 返回 下单页面
查看结果 要禁止 多次 跳转 失败成功 极限 循环 过->
13-15订单状态未成功修改的原因
支付时 成功失败 同时请求服务器更改 支付状态
视频:状态是微信支付api返回时才改的 不是客户端改的
自己模拟了
微信支付api 企业不行
域名 代理 回调 才行了
二不行 过 模拟
代理软件 不过
支付成功 订单页 显示支付 不显示支付点击
13-16 点击按钮返回订单详情页面
pay-result
查看订单
js
{
viewOrder:function(){
//查看订单
//思路在order.onShow 重写类似代码 跳转传参数
// wx.navigateTo({//可能 下单 支付 打开过order 只onload onShow 要参数
// url: '../order/order?id='+this.data.id,
// })
wx.navigateBack({
//返回上级页面 delta设置返回多少级页面 不用参数
delta:1
})
}
}
{
wx.navigateTo是跳转到 多层挑 不好
要传悌参数
wx.navigateBack返回上级
不用 参数
都可以
}
wx.navigateBack({
//返回上级页面 delta设置返回多少级页面
delta:1
})
13-17获取用户信息
pages/my
我的页面
wxml简单直接mv
wxss 本直接
wxml 注意一下
布局
数据绑定
css 详细本公
container
头部 微信api 获得
地址管理
历史订单
title
item
加载中
先测试 去掉
加载中...
没数据时 显示加载中
先wx.login->wx.getUserInfo
点击授权 在才显示
success fail拒绝授权
加载状态
loadding标签
导航栏 加载状态 --- ---- --->
没有图片 清除 缓存
文件 授权数据
手贱
获取 wx.getUserInfo 接口后续将不再出现授权弹窗,请注意升级
参考文档: https://developers.weixin.qq.com/blogdetail?action=get_post_info&lang=zh_CN&token=1650183953&docid=0000a26e1aca6012e896a517556c01
版本低 高 不出现授权
https://developers.weixin.qq.com/community/develop/doc/0000a26e1aca6012e896a517556c01
为优化用户体验,使用 wx.getUserInfo
接口直接弹出授权框的开发方式将逐步不再支持。
从2018年4月30日开始,小程序与小游戏的体验版、
开发版调用 wx.getUserInfo 接口,将无法弹出授权询问框,
默认调用失败。正式版暂不受影响。开发者可使用以下方式获取或展示用户信息:
升级 行了 不
办法
登录授权
刷新才行了
再配合js wx.getUserInfo 使用
HTML实现当前页面刷新
首先我们都知道在HTML页面我们要实现当前页面刷新一般会怎么解决呢?
1,reload()方法刷新当前页面;
2,replace() 方法刷新当前页面;
3,页面自动刷新当前页面;
this.onReady() 或者 this.onShow()(亲测有效)
this.onLoad() 有点问题
定义标志app.flag true onshow自动
https://www.cnblogs.com/guo-xu/p/10629225.html
button:onShow 不行直接调用生命周期函数
--.间接也不行了
onReady 都不行 可能是提示框出现 所以不行
可能不是bindtap ontap
bindchange
直接俩次onLoad 可以
间接俩次onLoad 可以
循环调用俩次 不行 要点击才执行
小程序新方法 open-type获取头像昵称
https://www.jianshu.com/p/e4657f16a82c
授权登录
bindgetuserinfo这个事件
Page({
data: {},
bindGetUserInfo: function(e) {
var that = this;
//此处授权得到userInfo
console.log(e.detail.userInfo);
//接下来写业务代码
//最后,记得返回刚才的页面
wx.navigateBack({
delta: 1 我的不行 onShow不行
})
}
})
bindGetphonenumber
e.detail.userInfo e 随便一个 这个比较全
open-type标签到时 先用 够了
小程序button组件因为很多关于用户信息的功能,
所以不可能自动触发,模拟点击也是不可能的,
不可能给你自动获取用户信息的,
比如之前的wx.getUserInfo接口不再出现授权弹窗,
请使用
引导用户主动进行授权操作,这类事件必须要用户主动进行
只能点击才能使用
客户端代码要改
不自动获得令牌
配套维持
wx.login code->servers->oppleid->sql->token
不管 wx.getuserinfo用户信息事
大不了不登录 操作
但不够友好 可以操作下单 但是又没登录
解决 1.自动获得token移到登录授权 手动
2.自动token加条件授权才获取token
3.1 2配合使用 加载页面时出现引导 if授权&&->token 再挑index
&&一块 ->先授权再token
{
toShow:function(event){
// console.log(event)
//button open-type="getUserInfo" bindgetuserinfo="toShow"
//只能点击才能使用
// 客户端代码要改
// 不自动获得令牌
// 配套维持
// wx.login code -> servers -> oppleid -> sql -> token
// 不管 wx.getuserinfo用户信息事
// 大不了不登录 操作
// 但不够友好 可以操作下单 但是又没登录
// 解决 1.自动获得token移到登录授权 手动
// 2.自动token加条件授权才获取token
// 3.1 2配合使用 加载页面时出现引导 if授权 && -> token 再挑index
// && 一块 -> 先授权再token
if(event.detail.userInfo){
this.onLoad();
}
},
}
小程序按钮点击css效果(你很需要!)
https://blog.csdn.net/weixin_42275932/article/details/83859013
hover-class=点击时样式
微信小程序Button怎么设置disabled时的样式?
button[disabled]
授权引导
https://www.jianshu.com/p/b704ac7b8955
https://blog.csdn.net/qq_33616529/article/details/79080141
检查 授权
wx.getSetting
if (res.authSetting['scope.userInfo'])
wx.openSetting 打开设置界面,引导用户开启授权。
授权:authorize
authorize页面
js
{
// pages/authorize/authorize.js
Page({
data: {
canIUse:wx.canIUse('button.open-type.getUserInfo')
},
bindGetUserInfo:function(event){
if (event.detail.userInfo) {
// wx.switchTab({
// url: '../home/home',
// })
//返回比较合理 home调用也行 app.js加载时也可以吧更好
wx.navigateBack({
delta:1
})
}else{
wx.showModal({
title: '拒绝授权',
content:'需要进入,请重新授权',
// showCancel:false,//让用户没选择
cancelText:'退出',
confirmText:'重新申请',
success(res){
if(res.cancel){
//微信小程序官方并没有提供退出的api。
//返回类似
wx.navigateBack({
delta:0
})
}
if(res.confirm){
//默认当前
}
}
})
}
}
})
}
wxml
{
开始体验授权
退出
请升级微信版本
}
css
{
/* pages/authorize/authorize.wxss */
.content{
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: #ccc;
z-index: 9;
}
button{
position: fixed;
bottom: 180rpx;
width: 360rpx;
height: 80rpx;
left: 50%;
transform: translateX(-50%);
font-size: 30rpx;
border:1px solid #fff;
border-radius: 20rpx;
z-index: 99;
}
.exit{
bottom: 90rpx;
}
}
home.js
{
onLoad: function (options) {
this._loadData();//不直接调用
wx.getSetting({
success: function (res) {
// 不存在 跳授权authorize
console.log('homeJs', res)
if (!res.authSetting['scope.userInfo']) {
wx.navigateTo({
url: '../authorize/authorize',
})
}
}
})
},
}
退出
返回
https://blog.csdn.net/weixin_43409286/article/details/88100961
微信小程序官方并没有提供退出的api。
wx.navigateBack({
delta: 0
})
app.json不行
不能跳转
home 主页设置 to 获取授权页 tohome
或者 获得再进入首页 返回to-tohome
微信小程序透明度 opacity
style="opacity:{{number}}"
background: rgba(0,0,0,0);
1、错误的写法
.mask {
background: black;
opacity: 0.5;
}
2、正确的写法
.mask {
background: rgba(0, 0, 0, 0.5);
}
background:url(' ..图片地址.. ')
pages/authorize/authorize.wxss
中的本地资源图片无法通过 WXSS 获取,
可以使用网络图片,或者 base64,或者使用 标签。请参考文档
{
/* pages/authorize/authorize.wxss */
.content{
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
/* background:url('background.jpg'); */
z-index: 9;
opacity: 0.5;
background-color: #ccc;
/* background: rgba(0, 0, 0, 0.5); */
}
button{
position: fixed;
bottom: 180rpx;
width: 360rpx;
height: 80rpx;
left: 50%;
transform: translateX(-50%);
font-size: 30rpx;
border:1px solid #fff;
border-radius: 20rpx;
z-index: 99;
}
.exit{
bottom: 90rpx;
}
}
image{
height: 100%;
width: 100%;
position: fixed;
opacity: 0.8;
z-index: 1;
}
css 改颜色 鼠标见过 预览 设置
navigate标签可以取消返回 不授权不返回 手机返回 不用
wx.nav..未知
过
有无 可以 过
重启动软件 程序 pc
以前直接2017 2018改 2019->改了
获得时间改了
用户信息
my.js
{
// pages/my/my.js
import {My} from 'my-model.js';
import {Order} from '../order/order-model.js';
import {Address} from '../../utils/address.js';
var my=new My();
var order=new Order();
var address=new Address();
Page({
data: {
on:false
},
onLoad: function (options) {
// wx.openSetting({ 不行 未知
// success(res){
// console.log('openSetting',res)
// }
// })
this._loadData();
var that=this;
wx.getSetting({
success:function(res){
// console.log(res);
if (res.authSetting['scope.userInfo']){
// that._loadData(); toShow重新onload
that.setData({
on: res.authSetting['scope.userInfo']
});
}
}
})
},
_loadData:function(){
my.getUserInfo((data)=>{
this.setData({
userInfo:data
});
});
},
toShow:function(event){
// console.log(event)
//button open-type="getUserInfo" bindgetuserinfo="toShow"
//只能点击才能使用
// 客户端代码要改
// 不自动获得令牌
// 配套维持
// wx.login code -> servers -> oppleid -> sql -> token
// 不管 wx.getuserinfo用户信息事
// 大不了不登录 操作
// 但不够友好 可以操作下单 但是又没登录
// 解决 1.自动获得token移到登录授权 手动
// 2.自动token加条件授权才获取token
// 3.1 2配合使用 加载页面时出现引导 if授权 && -> token 再挑index
// && 一块 -> 先授权再token
if(event.detail.userInfo){
this.onLoad();
this.setData({on:true})
}
},
// token:function(event){
// console.log(event)
// wx.login({
// success:function(res){
// var code=res.code;
// wx.request({
// url: 'http://z.cn/api/v1/token/user',//get 在url
// data: {
// code: code//post data
// },
// method: 'POST',
// success:function(res){
// wx.setStorageSync('token', res.data.token);
// }
// })
// }
// })
// },
})
}
my-model.js
{
import { Base } from '../../utils/base.js';
class My extends Base{
constructor(){
super();
}
//得到用户微信信息
getUserInfo(cb){
var that=this;
wx.login({
success:function(res){
wx.getUserInfo({
success:function(res){
console.log(res)
typeof cb=="function"&&cb(res.userInfo);
},
fail: function (res) {//拒绝授权
console.log(res)
typeof cb=="function"&&cb({
avatarUrl:'../../imgs/icon/user@default.png',
nickName:'零食小贩'
});
}
})
}
})
}
}
export {My};
}
my.wxml
{
地址管理
+ 添加地址
我的订单
{{item.snap_name}}
{{item.total_count}}件商品
待付款
已付款
已发货
实付:¥{{item.total_price}}
付款
}
{{on?'已登录':'登录授权刷新'}}
13-18显示用户收货地址
先加载数据 初始化显示数据 再调试 事件等等
获得用户地址信息 order.address模型 已经写了
引用 getAddress
七月 小程序课程 ->
this.data='' 仅赋值 不刷新 旧版可以
等价
this.setData 重新数据绑定 页面刷新
官网文档 最新 参考 有时看看
数据绑定 生效
但js 赋值 可以用
my.js
{
// pages/my/my.js
import {My} from 'my-model.js';
import {Order} from '../order/order-model.js';
import {Address} from '../../utils/address.js';
var my=new My();
var order=new Order();
var address=new Address();
Page({
data: {
on:false
},
onLoad: function (options) {
// wx.openSetting({ 不行 未知
// success(res){
// console.log('openSetting',res)
// }
// })
this._loadData();
this._getAddressInfo();
var that=this;
wx.getSetting({
success:function(res){
// console.log(res);
if (res.authSetting['scope.userInfo']){
// that._loadData(); toShow重新onload
that.setData({
on: res.authSetting['scope.userInfo']
});
}
}
})
},
_getAddressInfo:function(){
//获得地址
address.getAddress((addressInfo)=>{
this._bindAddressInfo(addressInfo);
});
},
//绑定地址信息
_bindAddressInfo:function(addressInfo){
// this.data.addressInfo = addressInfo; 等价下 但新版刷新页面
this.setData({
addressInfo:addressInfo
});
},
_loadData:function(){
my.getUserInfo((data)=>{
this.setData({
userInfo:data
});
});
},
toShow:function(event){
// console.log(event)
//button open-type="getUserInfo" bindgetuserinfo="toShow"
//只能点击才能使用
// 客户端代码要改
// 不自动获得令牌
// 配套维持
// wx.login code -> servers -> oppleid -> sql -> token
// 不管 wx.getuserinfo用户信息事
// 大不了不登录 操作
// 但不够友好 可以操作下单 但是又没登录
// 解决 1.自动获得token移到登录授权 手动
// 2.自动token加条件授权才获取token
// 3.1 2配合使用 加载页面时出现引导 if授权 && -> token 再挑index
// && 一块 -> 先授权再token
if(event.detail.userInfo){
this.onLoad();
this.setData({on:true})
}
},
// token:function(event){
// console.log(event)
// wx.login({
// success:function(res){
// var code=res.code;
// wx.request({
// url: 'http://z.cn/api/v1/token/user',//get 在url
// data: {
// code: code//post data
// },
// method: 'POST',
// success:function(res){
// wx.setStorageSync('token', res.data.token);
// }
// })
// }
// })
// },
})
}
13-19初步显示历史订单
初步显示历史订单
_loadData->_getOrders-order-model.js
加载 历史订单
编写
同步接收 异步接受 回调方法
var data=home.getBannerData(id,this.callBack);回调方法
模拟支付成功 改数据库 历史订单
my.js
{
// pages/my/my.js
import {My} from 'my-model.js';
import {Order} from '../order/order-model.js';
import {Address} from '../../utils/address.js';
var my=new My();
var order=new Order();
var address=new Address();
Page({
data: {
on:false,
pageIndex:1,//分页加载 加载1页15个default
},
onLoad: function (options) {
// wx.openSetting({ 不行 未知
// success(res){
// console.log('openSetting',res)
// }
// })
this._loadData();
this._getAddressInfo();
var that=this;
wx.getSetting({
success:function(res){
// console.log(res);
if (res.authSetting['scope.userInfo']){
// that._loadData(); toShow重新onload
that.setData({
on: res.authSetting['scope.userInfo']
});
}
}
})
},
_getAddressInfo:function(){
//获得地址
address.getAddress((addressInfo)=>{
this._bindAddressInfo(addressInfo);
});
},
//绑定地址信息
_bindAddressInfo:function(addressInfo){
// this.data.addressInfo = addressInfo; 等价下 但新版刷新页面
this.setData({
addressInfo:addressInfo
});
},
_loadData:function(){
my.getUserInfo((data)=>{
this.setData({
userInfo:data
});
});
this._getOrders();
},
_getOrders:function(){
order.getOrders(this.data.pageIndex,(res)=>{
// console.log(res.data);
var data=res.data.data;//res.data 服务器 wx版本
this.setData({
orderArr:data
})
});
},
toShow:function(event){
// console.log(event)
//button open-type="getUserInfo" bindgetuserinfo="toShow"
//只能点击才能使用
// 客户端代码要改
// 不自动获得令牌
// 配套维持
// wx.login code -> servers -> oppleid -> sql -> token
// 不管 wx.getuserinfo用户信息事
// 大不了不登录 操作
// 但不够友好 可以操作下单 但是又没登录
// 解决 1.自动获得token移到登录授权 手动
// 2.自动token加条件授权才获取token
// 3.1 2配合使用 加载页面时出现引导 if授权 && -> token 再挑index
// && 一块 -> 先授权再token
if(event.detail.userInfo){
this.onLoad();
this.setData({on:true})
}
},
// token:function(event){
// console.log(event)
// wx.login({
// success:function(res){
// var code=res.code;
// wx.request({
// url: 'http://z.cn/api/v1/token/user',//get 在url
// data: {
// code: code//post data
// },
// method: 'POST',
// success:function(res){
// wx.setStorageSync('token', res.data.token);
// }
// })
// }
// })
// },
})
}
order-model.js
{
//获得所有订单,pageIndex 从1开始 分页加载
getOrders(pageIndex,callBack){
var allParams={
url:'order/by_user',
data:{
page:pageIndex,
// size:15,
},
type:'get',//可省略 base.js
sCallBack: function (data) {
callBack && callBack(data);
},
eCallBack: function () { }
};
this.request(allParams);
}
}
my.wxml
{
我的订单
{{item.snap_name}}
{{item.total_count}}件商品
待付款
已付款
已发货
实付:¥{{item.total_price}}
付款
}
问题
重新下单 不同步 加载历史订单 刷新才
下单是 重新my.onload() reload
13-20分页加载历史订单数据
分页加载历史订单数据
滚动时 加载 其页面
小程序 滚动生命周期函数
下拉触底函数 好像还要在json 字段声明
在调用
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
json声明
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
好像不用
},
传统dom
到底 加载 第二条 数据
追加打数组 重新显示
但是 其他 服务器 又加 限制 自己page+1 不行
数组问题 appdata 数据绑定 解决问题
https://blog.csdn.net/lyz86299355/article/details/82686449
var a = {}
Object.keys(a) // []
if (Object.keys(object).length === 0) {
return false // 如果为空,返回false
}
return true // 如果不为空,则会执行到这一步,返回true
微信小程序undefined判断
typeof(tmp) == "undefined"
app.globalData.userInfo['NickName'] != undefined
判断undefined、null与NaN:
if (!tmp)
https://www.php.cn/blog/detail/14054.html
// for(let i=0;i{
this._bindAddressInfo(addressInfo);
});
},
//绑定地址信息
_bindAddressInfo:function(addressInfo){
// this.data.addressInfo = addressInfo; 等价下 但新版刷新页面
this.setData({
addressInfo:addressInfo
});
},
_loadData:function(){
my.getUserInfo((data)=>{
this.setData({
userInfo:data
});
});
this._getOrders();
},
_getOrders:function(){
order.getOrders(this.data.pageIndex,(res)=>{
if(Object.keys(res.data).length==0){
//res.data 服务器 wx版本
}
var data = res.data.data;
// if (data.length > 0)
// console.log(Object.keys(res.data).length!=0);
if (Object.keys(res.data).length != 0){
// for(let i=0;i
this.onLoad();
console.log('下拉动作');//json字段声明appWin字段可能 page.json 全局
//json "enablePullDownRefresh": true
wx.stopPullDownRefresh();
//当处理完数据刷新后,wx.stopPullDownRefresh可以停止当前页面的下拉刷新。
// if2次停止计数器可以停止当前页面的下拉刷新。
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
console.log('上拉触底');//json改底部距离 容易触发
// "onReachBottomDistance": 100 json
//当界面的下方距离页面底部距离小于100像素时触发回调
// this.setData({//追加打数组 重新显示
// pageIndex: this.data.pageIndex + 1//服务器 又加 限制 自己page+1 不行
// })
// this._getOrders();
if(!this.data.isLoadedAll){
//是否加载完成 false没加载完
this.data.pageIndex++;
this._getOrders();
}
// scroll - view 下拉功能
console.log('是否加载true完成:',this.data.isLoadedAll)
},
toShow:function(event){
// console.log(event)
//button open-type="getUserInfo" bindgetuserinfo="toShow"
//只能点击才能使用
// 客户端代码要改
// 不自动获得令牌
// 配套维持
// wx.login code -> servers -> oppleid -> sql -> token
// 不管 wx.getuserinfo用户信息事
// 大不了不登录 操作
// 但不够友好 可以操作下单 但是又没登录
// 解决 1.自动获得token移到登录授权 手动
// 2.自动token加条件授权才获取token
// 3.1 2配合使用 加载页面时出现引导 if授权 && -> token 再挑index
// && 一块 -> 先授权再token
if(event.detail.userInfo){
this.onLoad();
this.setData({on:true})
}
},
// token:function(event){
// console.log(event)
// wx.login({
// success:function(res){
// var code=res.code;
// wx.request({
// url: 'http://z.cn/api/v1/token/user',//get 在url
// data: {
// code: code//post data
// },
// method: 'POST',
// success:function(res){
// wx.setStorageSync('token', res.data.token);
// }
// })
// }
// })
// },
})
}
order-model.js
{
import { Base } from '../../utils/base.js';
class Order extends Base{
constructor(){
super();
this._storageKeyName='newOrder';
}
//下订单
doOrder(param,callBack){
var that=this;
var allParams={
url:'order',
type:'post',
data:{
products:param
},
sCallBack:function(data){
that.execSetStorageSync(true);
callBack&&callBack(data);
},
eCallBack:function(){}
};
this.request(allParams);
}
/**
* 拉起微信支付
* params:
* norderNumber-{int}订单id
* return:
* callback-{obj} 回调方法,返回参数
* 可能值0:商品缺货等原因导致订单不能支付;
* 1:支付失败或者支付取消
* 2:支付成功
*/
execPay(orderNumber,callBack){
var allParams={
url:'pay/pre_order',
type:'post',
data:{
id:orderNumber
},
sCallBack:function(data){
console.log('服务器返回:',data);
var timeStamp=data.timeStamp;//服务器返回的时间戳
if(timeStamp){//服务器返回的订单详细 存在 才支付
wx.requestPayment({//微信支付api
timeStamp: timeStamp.toString(),
nonceStr: data.nonceStr,
package: data.package,
signType: data.signType,
paySign: data.paySign,
success:function(){
callBack&&callBack(2);//更新订单支付状态 2支付成功
},
fail:function(){
callBack && callBack(1);//支付失败或者支付取消
//服务器 模拟返回可以在base==500 模拟
// 预防其他问题 测试完 改变
// 小程序 返回结果比较难 改状态 模拟 当无 过
// 思路
// callBack && callBack(2);
// 除非 反过来调试
// success 也是返回2 成功 或者反过来1
// 不用模拟成功失败
// order.js _execPay statusCode != 0 都执行
// fail = 1 success = 2 够用 可以不改 不是0就行了
// fail api本身就失败 测试失败当成功1变2 测试
}
})
}
else{
callBack && callBack(0);//更新订单支付状态 0没下单没支付
}
}
};
// console.log(allParams);
this.request(allParams);
}
//支付时 成功失败 同时请求服务器更改 支付状态
_toServiceStatus(status){
var params={
// api接口没编写 未知
}
this.request(params);
}
//获得订单的具体内容 服务器数据库中 订单快照
getOrderInfoById(id,callBack){
var that=this;
var allParams={
url:'order/'+id,
sCallBack:function(data){
callBack&&callBack(data);
},
eCallBack:function(){}
};
this.request(allParams);
}
//获得所有订单,pageIndex 从1开始 分页加载
getOrders(pageIndex,callBack){
var allParams={
url:'order/by_user',
data:{
page:pageIndex,
// size:15,
},
type:'get',//可省略 base.js
sCallBack: function (data) {
callBack && callBack(data);
},
eCallBack: function () { }
};
this.request(allParams);
}
//本地缓存 保存/更新
execSetStorageSync(data){
wx.setStorageSync(this._storageKeyName, data);//true 下单了
}
}
export{Order};
}
my.wxml
{
地址管理
+ 添加地址
我的订单
{{item.snap_name}}
{{item.total_count}}件商品
待付款
已付款
已发货
实付:¥{{item.total_price}}
付款
}
分销 小程序 公众号 商店 用户 打$
数据库 一对一 分 显示
13-21重构订单详情页面
我的页面 进入 订单详情页面
购物车cart 历史订单order
封装 重构 cart跳转到订单详情页面
主方法 简洁 层次感
订单详情页面 未支付 再支付 出问题
因为订单号已经 成功
未支付
判断 订单号 存在 调用订单号 在支付....
过 麻烦
debug
下拉触底
点击 详情页面 不显示 金额 图片等
以前保存 时 就 没快照 所以没有
上啦全部刷新 合理 刷新嘛
my.js
{
// pages/my/my.js
import {My} from 'my-model.js';
import {Order} from '../order/order-model.js';
import {Address} from '../../utils/address.js';
var my=new My();
var order=new Order();
var address=new Address();
Page({
data: {
on:false,
pageIndex:1,//分页加载 加载1页15个default
orderArr:[],
isLoadedAll:false,//是否追加完
},
onLoad: function (options) {
// wx.openSetting({ 不行 未知
// success(res){
// console.log('openSetting',res)
// }
// })
this._loadData();
this._getAddressInfo();
var that=this;
wx.getSetting({
success:function(res){
// console.log(res);
if (res.authSetting['scope.userInfo']){
// that._loadData(); toShow重新onload
that.setData({
on: res.authSetting['scope.userInfo']
});
}
}
})
},
_getAddressInfo:function(){
//获得地址
address.getAddress((addressInfo)=>{
this._bindAddressInfo(addressInfo);
});
},
//绑定地址信息
_bindAddressInfo:function(addressInfo){
// this.data.addressInfo = addressInfo; 等价下 但新版刷新页面
this.setData({
addressInfo:addressInfo
});
},
_loadData:function(){
my.getUserInfo((data)=>{
this.setData({
userInfo:data
});
});
this._getOrders();
},
_getOrders:function(){
order.getOrders(this.data.pageIndex,(res)=>{
if(Object.keys(res.data).length==0){
//res.data 服务器 wx版本
}
var data = res.data.data;
// if (data.length > 0)
// console.log(Object.keys(res.data).length!=0);
if (Object.keys(res.data).length != 0){
// for(let i=0;i
this.onLoad();//数组重复加载 除非onload data重新 过
// this._getOrders();
console.log('下拉动作');//json字段声明appWin字段可能 page.json 全局
//json "enablePullDownRefresh": true
wx.stopPullDownRefresh();
//当处理完数据刷新后,wx.stopPullDownRefresh可以停止当前页面的下拉刷新。
// if2次停止计数器可以停止当前页面的下拉刷新。
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
console.log('上拉触底');//json改底部距离 容易触发
// "onReachBottomDistance": 100 json
//当界面的下方距离页面底部距离小于100像素时触发回调
// this.setData({//追加打数组 重新显示
// pageIndex: this.data.pageIndex + 1//服务器 又加 限制 自己page+1 不行
// })
// this._getOrders();
if(!this.data.isLoadedAll){
//是否加载完成 false没加载完
this.data.pageIndex++;
this._getOrders();
}
else{
wx.showToast({
title: '已经全部显示历史订单',
})
}
// scroll - view 下拉功能
console.log('是否加载true完成:',this.data.isLoadedAll)
},
//显示订单的具体信息
showOrderDetailInfo:function(event){
var id=order.getDataSet(event,'id');//数据库标签传递来的订单号id
wx.navigateTo({
url: '../order/order?from=order&id='+id,
});
},
toShow:function(event){
// console.log(event)
//button open-type="getUserInfo" bindgetuserinfo="toShow"
//只能点击才能使用
// 客户端代码要改
// 不自动获得令牌
// 配套维持
// wx.login code -> servers -> oppleid -> sql -> token
// 不管 wx.getuserinfo用户信息事
// 大不了不登录 操作
// 但不够友好 可以操作下单 但是又没登录
// 解决 1.自动获得token移到登录授权 手动
// 2.自动token加条件授权才获取token
// 3.1 2配合使用 加载页面时出现引导 if授权 && -> token 再挑index
// && 一块 -> 先授权再token
if(event.detail.userInfo){
this.onLoad();
this.setData({on:true})
}
},
// token:function(event){
// console.log(event)
// wx.login({
// success:function(res){
// var code=res.code;
// wx.request({
// url: 'http://z.cn/api/v1/token/user',//get 在url
// data: {
// code: code//post data
// },
// method: 'POST',
// success:function(res){
// wx.setStorageSync('token', res.data.token);
// }
// })
// }
// })
// },
})
}
order.js
{
// pages/order/order.js
import {Cart} from '../cart/cart-model.js';
import {Order} from 'order-model.js';
import {Address} from '../../utils/address.js';
var cart=new Cart();
var order=new Order();
var address=new Address();
Page({
/**
* 页面的初始数据
*/
data: {
id:null,//订单号id onshow2 -1 null
},
/**
* 生命周期函数--监听页面加载
*/
onLoad: function (options) {
// var productsArr = cart.getCartDataFromLocal(true);//从缓存读取商品 model.js
// this.data.account=options.account;
// this.data.id=options.id;//wx.navigateTopay-result传悌 back不用
// this.data.productsArr=productsArr;页面不同步 setData才同步数据绑定
//请求服务器address地址
// var param={
// //地址未知 下订单 快照等 复杂过 配套当时
// };
// address.request(param);
// this.setData({ 重构到_fromCart
// id:options.id,
// productsArr:productsArr,
// account:options.account,
// orderStatus:0 //是否下单
// });
// //显示收货地址
// address.getAddress((res)=>{
// this._bindAddressInfo(res);//重新数据绑定
// });
var from=options.from;
if(from=='cart'){//cart 本地加载
this._fromCart(options.account);
}
else if(from=='order'){//else if也行 order服务器加载
this._fromOrder(options.id);//id抽离
}
},
_fromOrder:function(id){
if (id) {//订单号 id存在 debug
var that = this;
// var id = this.data.id;
order.getOrderInfoById(id, (data) => {
that.setData({
orderStatus: data.status,//数据库字段status 支付状态 改2 成功2
productsArr: data.snap_items,
account: data.total_price,
basicInfo: {
orderTime: data.create_time,
orderNo: data.order_no
}
});
//快照地址
var addressInfo = data.snap_address;
addressInfo.totalDetail = address.setAddressInfo(addressInfo);
that._bindAddressInfo(addressInfo);
});
}
},
_fromCart:function(account){
var productsArr = cart.getCartDataFromLocal(true);//从缓存读取商品 model.js
this.data.account = account;
this.setData({
productsArr: productsArr,
account: account,
orderStatus: 0 //是否下单
});
//显示收货地址
address.getAddress((res) => {
this._bindAddressInfo(res);//重新数据绑定
});
},
//下单后, 支付成功或者失败后,点左上角返回时能够更新订单状态
//所以放在onshow中
onShow:function(){
if (this.data.id) {//order服务器加载
this._fromOrder(this.data.id);
}
// console.log(options) 没 重构到_fromOrder
// if(this.data.id){//订单号 id存在 debug
// var that=this;
// var id=this.data.id;
// order.getOrderInfoById(id,(data)=>{
// that.setData({
// orderStatus:data.status,//数据库字段status 支付状态 改2 成功2
// productsArr:data.snap_items,
// account:data.total_price,
// basicInfo:{
// orderTime:data.create_time,
// orderNo:data.order_no
// }
// });
// //快照地址
// var addressInfo=data.snap_address;
// addressInfo.totalDetail=address.setAddressInfo(addressInfo);
// that._bindAddressInfo(addressInfo);
// });
// }
},
editAddress:function(event){
//获得地址 编辑地址 wxapi组件
var that=this;//回调函数this改变
wx.chooseAddress({
success:function(res){
// console.log(address.setAddressInfo(res))
var addressInfo={
name:res.userName,
mobile:res.telNumber,//模拟的电话不符合服务器
totalDetail:address.setAddressInfo(res) //../../utils/address.js model拼合
};
that._bindAddressInfo(addressInfo);
//保存地址
address.submitAddress(res,(flag)=>{
if(!flag){
that.showTips('操作提示','地址信息更新失败!');
}
})
}
})
},
/**
* 提示窗口
* params:title-{string}标题
* content-{string}内容
* flag-{bool}是否跳转到"我的页面"
*/
showTips:function(title,content,flag){
wx.showModal({
title: title,
content: content,
showCancel:false,//取消按钮hidden
success:function(res){
if(flag){
wx.switchTab({
url: '../my/my',// /pages/my/my
});
}
}
})
},
/**
* 绑定地址信息
*/
_bindAddressInfo:function(addressInfo){
// console.log(addressInfo);
this.setData({
addressInfo:addressInfo
});
},
//下单和付款
pay:function(){
if(!this.data.addressInfo){
this.showTips('下单提示','请填写您的收货地址');
return;
}
//写在一起 历史订单 点击 和 order页一样
if(this.data.orderStatus==0){
this._firstTimePay();//下单页 支付 服务器订单号还没创建
}
else{
this._oneMoresTimePay();//历史订单 支付 服务器订单号已创建
}
},
//第一次支付 order下单页
_firstTimePay:function(){
var orderInfo=[],
productInfo=this.data.productsArr,
order=new Order();
// console.log(this.data.productsArr)
for(let i=0;i{
//订单生成成功 服务器订单号 成功
console.log('订单生成成功:',data);
if(data.pass){
//更新订单状态 order字段status 1 2 3 4
var id = data.order_id;//服务器返回的订单号id
that.data.id=id; //订单号 id
// that.data.fromCartFlag=false;//下单页
//开始支付 在orderModel
that._execPay(id);
}
else{//data.pass=false 库存量订单不通过
that._orderFail(data);//下单失败 商品id和商品返回
}
});
},
//下单失败 params:data-{obj} 订单结果信息
_orderFail:function(data){
var nameArr=[],
name='',
str='',
pArr=data.pStatusArray;//商品快照信息
for(let i=0;i15){
name=name.substr(0,12)+'...';
}
nameArr.push(name);
if(nameArr.length>=2){
break;
}
}
}
str += nameArr.join('、');
if(nameArr.length>0){
str+=' 等';
}
str+=' 缺货';
wx.showModal({
title: '下单失败',
content:str,
showCancel:false,
success:function(res){}
})
},
//开始支付 params:id-{int} 订单id
_execPay:function(id){
var that=this;
order.execPay(id,(statusCode)=>{
console.log('支付成功状态2成功1取消失败0没支付:',statusCode)
if(statusCode!=0){
//将已经下单的商品从购物车删除 当状态为0时,表示下单支付失败
that.deleteProducts();
var flag=statusCode==2;//=2表示支付成功
wx.navigateTo({//下单成功 挑战 支付成功页面
url: '../pay-result/pay-result?id='+id+'&flag='+flag+'&from=order',
});
}
});
},
//将已经下单的商品从购物车删除
deleteProducts:function(){
var ids=[],arr=this.data.productsArr;
for(let i=0;i
历史订单
二次支付
wxml
{
实付:¥{{item.total_price}}
付款
}
事件 建议 写 用不用 可以不可以
购物车订单 我的没有实时
会讲过
历史订单 进入 二次支付 error 未知进 自己到时 都过
因为订单号已经 成功
未支付
小程序->服务器->微信支付api
有订单 1-3 没企业微信api不能 正确返回 1->3
服务端问题 不管小程序 好像是
但是 反应 wx.x.fail->1 应该可以在次支付 TODO ->
my.js
[
// pages/my/my.js
import {My} from 'my-model.js';
import {Order} from '../order/order-model.js';
import {Address} from '../../utils/address.js';
var my=new My();
var order=new Order();
var address=new Address();
Page({
data: {
on:false,
pageIndex:1,//分页加载 加载1页15个default
orderArr:[],
isLoadedAll:false,//是否追加完
},
onLoad: function (options) {
// wx.openSetting({ 不行 未知
// success(res){
// console.log('openSetting',res)
// }
// })
this._loadData();
this._getAddressInfo();
var that=this;
wx.getSetting({
success:function(res){
// console.log(res);
if (res.authSetting['scope.userInfo']){
// that._loadData(); toShow重新onload
that.setData({
on: res.authSetting['scope.userInfo']
});
}
}
})
},
_getAddressInfo:function(){
//获得地址
address.getAddress((addressInfo)=>{
this._bindAddressInfo(addressInfo);
});
},
//绑定地址信息
_bindAddressInfo:function(addressInfo){
// this.data.addressInfo = addressInfo; 等价下 但新版刷新页面
this.setData({
addressInfo:addressInfo
});
},
_loadData:function(){
my.getUserInfo((data)=>{
this.setData({
userInfo:data
});
});
this._getOrders();
},
_getOrders:function(){
order.getOrders(this.data.pageIndex,(res)=>{
if(Object.keys(res.data).length==0){
//res.data 服务器 wx版本
}
var data = res.data.data;
// if (data.length > 0)
// console.log(Object.keys(res.data).length!=0);
if (Object.keys(res.data).length != 0){
// for(let i=0;i
this.onLoad();//数组重复加载 除非onload data重新 过
// this._getOrders();
console.log('下拉动作');//json字段声明appWin字段可能 page.json 全局
//json "enablePullDownRefresh": true
wx.stopPullDownRefresh();
//当处理完数据刷新后,wx.stopPullDownRefresh可以停止当前页面的下拉刷新。
// if2次停止计数器可以停止当前页面的下拉刷新。
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
console.log('上拉触底');//json改底部距离 容易触发
// "onReachBottomDistance": 100 json
//当界面的下方距离页面底部距离小于100像素时触发回调
// this.setData({//追加打数组 重新显示
// pageIndex: this.data.pageIndex + 1//服务器 又加 限制 自己page+1 不行
// })
// this._getOrders();
if(!this.data.isLoadedAll){
//是否加载完成 false没加载完
this.data.pageIndex++;
this._getOrders();
}
else{
wx.showToast({
title: '已经全部显示历史订单',
})
}
// scroll - view 下拉功能
console.log('是否加载true完成:',this.data.isLoadedAll)
},
//显示订单的具体信息
showOrderDetailInfo:function(event){
var id=order.getDataSet(event,'id');//数据库标签传递来的订单号id
wx.navigateTo({
url: '../order/order?from=order&id='+id,
});
},
rePay:function(event){
var id=order.getDataSet(event,'id');
index=order.getDataSet(event,'index');//orderArr下标
this._execPay(id,index);
},
_execPay:function(id,index){
var that=this;
order.execPay(id,(statusCode)=>{
if(statusCode>0){
var flag=statusCode==2;
//更新订单显示状态
if(flag){
that.data.orderArr[index].status=2;
that.setData({
orderArr:that.data.orderArr
});
}
//跳转到 成功页面
wx.navigateTo({
url: '../pay-result/pay-result?id='+id+'&flag='+flag+'&from=my',
});
}
else{
that.showTips('支付失败','商品已下架或库存不足');
}
});
},
//提示窗口 params:title-{string}标题 content-{string}内容 flag-{bool}是否跳转到'我的页面'
showTips:function(title,content){
wx.showModal({
title: title,
content: content,
showCancel:false,
success:function(res){
console.log(res.confirm,res.cancel);
}
})
},
toShow:function(event){
// console.log(event)
//button open-type="getUserInfo" bindgetuserinfo="toShow"
//只能点击才能使用
// 客户端代码要改
// 不自动获得令牌
// 配套维持
// wx.login code -> servers -> oppleid -> sql -> token
// 不管 wx.getuserinfo用户信息事
// 大不了不登录 操作
// 但不够友好 可以操作下单 但是又没登录
// 解决 1.自动获得token移到登录授权 手动
// 2.自动token加条件授权才获取token
// 3.1 2配合使用 加载页面时出现引导 if授权 && -> token 再挑index
// && 一块 -> 先授权再token
if(event.detail.userInfo){
this.onLoad();
this.setData({on:true})
}
},
// token:function(event){
// console.log(event)
// wx.login({
// success:function(res){
// var code=res.code;
// wx.request({
// url: 'http://z.cn/api/v1/token/user',//get 在url
// data: {
// code: code//post data
// },
// method: 'POST',
// success:function(res){
// wx.setStorageSync('token', res.data.token);
// }
// })
// }
// })
// },
})
]
13-23避免重复刷新订单列表
购物车订单 我的没有实时
my onShow 刷新 有新订单才刷新
刷新了 要 还原 下单状态
否则 多次请求
order-model
下单时 true doOrder
{
onShow:function(){
if(order.hasNewOrder){
this.refresh();
}//直接加载 每次show加载 卡 消耗性能 orderModel 判断有新时才加载
},
refresh:function(){
//重新加载
var that=this;
this.data.orderArr=[];//订单初始化
this._getOrders(()=>{
console.log('refresh');//network测试
that.data.isLoadedAll=false;//是否加载完成
this.data.pageIndex=1;
order.execSetStorageSync(false);//更新标志位
});
},
}
{
//是否有新的订单
hasNewOrder(){
var flag = wx.getStorageSync(this._storageKeyName);//下单了缓存true没false execSetStorageSync
return flag==true;
}
}
my.js
{
// pages/my/my.js
import {My} from 'my-model.js';
import {Order} from '../order/order-model.js';
import {Address} from '../../utils/address.js';
var my=new My();
var order=new Order();
var address=new Address();
Page({
data: {
on:false,
pageIndex:1,//分页加载 加载1页15个default
orderArr:[],
isLoadedAll:false,//是否追加完
},
onLoad: function (options) {
// wx.openSetting({ 不行 未知
// success(res){
// console.log('openSetting',res)
// }
// })
this._loadData();
this._getAddressInfo();
var that=this;
wx.getSetting({
success:function(res){
// console.log(res);
if (res.authSetting['scope.userInfo']){
// that._loadData(); toShow重新onload
that.setData({
on: res.authSetting['scope.userInfo']
});
}
}
})
},
_getAddressInfo:function(){
//获得地址
address.getAddress((addressInfo)=>{
this._bindAddressInfo(addressInfo);
});
},
//绑定地址信息
_bindAddressInfo:function(addressInfo){
// this.data.addressInfo = addressInfo; 等价下 但新版刷新页面
this.setData({
addressInfo:addressInfo
});
},
_loadData:function(){
my.getUserInfo((data)=>{
this.setData({
userInfo:data
});
});
this._getOrders();
},
_getOrders:function(callBack){
order.getOrders(this.data.pageIndex,(res)=>{
if(Object.keys(res.data).length==0){
//res.data 服务器 wx版本
}
var data = res.data.data;
// if (data.length > 0)
// console.log(Object.keys(res.data).length!=0);
if (Object.keys(res.data).length != 0){
// for(let i=0;i{
console.log('refresh');//network测试
that.data.isLoadedAll=false;//是否加载完成
this.data.pageIndex=1;
order.execSetStorageSync(false);//更新标志位
});
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
//解决 下单刷新 历史订单 刷新不自动返回-TODO->
// this.onLoad();//数组重复加载 除非onload data重新 过 onShow解决
this.onShow();//this.refresh
// this._getOrders();
console.log('下拉动作');//json字段声明appWin字段可能 page.json 全局
//json "enablePullDownRefresh": true
wx.stopPullDownRefresh();
//当处理完数据刷新后,wx.stopPullDownRefresh可以停止当前页面的下拉刷新。
// if2次停止计数器可以停止当前页面的下拉刷新。
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
console.log('上拉触底');//json改底部距离 容易触发
// "onReachBottomDistance": 100 json
//当界面的下方距离页面底部距离小于100像素时触发回调
// this.setData({//追加打数组 重新显示
// pageIndex: this.data.pageIndex + 1//服务器 又加 限制 自己page+1 不行
// })
// this._getOrders();
if(!this.data.isLoadedAll){
//是否加载完成 false没加载完
this.data.pageIndex++;
this._getOrders();
}
else{
wx.showToast({
title: '已经全部显示历史订单',
})
}
// scroll - view 下拉功能
console.log('是否加载true完成:',this.data.isLoadedAll)
},
//显示订单的具体信息
showOrderDetailInfo:function(event){
var id=order.getDataSet(event,'id');//数据库标签传递来的订单号id
wx.navigateTo({
url: '../order/order?from=order&id='+id,
});
},
rePay:function(event){
var id = order.getDataSet(event,'id'),
index = order.getDataSet(event,'index');//orderArr下标
this._execPay(id,index);
},
_execPay:function(id,index){
var that=this;
order.execPay(id,(statusCode)=>{
console.log('状态:',statusCode);
statusCode=2;//测试成功2 1失败 0不足 临时赋值 刷新恢复 数据库没改状态
if(statusCode>0){
var flag=statusCode==2;
//更新订单显示状态
if(flag){
that.data.orderArr[index].status=2;
that.setData({
orderArr:that.data.orderArr
});
}
//跳转到 成功页面
wx.navigateTo({
url: '../pay-result/pay-result?id='+id+'&flag='+flag+'&from=my',
});
}
else{
that.showTips('支付失败','商品已下架或库存不足');
}
});
},
//提示窗口 params:title-{string}标题 content-{string}内容 flag-{bool}是否跳转到'我的页面'
showTips:function(title,content){
wx.showModal({
title: title,
content: content,
showCancel:false,
success:function(res){
console.log(res.confirm,res.cancel);
}
})
},
toShow:function(event){
// console.log(event)
//button open-type="getUserInfo" bindgetuserinfo="toShow"
//只能点击才能使用
// 客户端代码要改
// 不自动获得令牌
// 配套维持
// wx.login code -> servers -> oppleid -> sql -> token
// 不管 wx.getuserinfo用户信息事
// 大不了不登录 操作
// 但不够友好 可以操作下单 但是又没登录
// 解决 1.自动获得token移到登录授权 手动
// 2.自动token加条件授权才获取token
// 3.1 2配合使用 加载页面时出现引导 if授权 && -> token 再挑index
// && 一块 -> 先授权再token
if(event.detail.userInfo){
this.onLoad();
this.setData({on:true})
}
},
// token:function(event){
// console.log(event)
// wx.login({
// success:function(res){
// var code=res.code;
// wx.request({
// url: 'http://z.cn/api/v1/token/user',//get 在url
// data: {
// code: code//post data
// },
// method: 'POST',
// success:function(res){
// wx.setStorageSync('token', res.data.token);
// }
// })
// }
// })
// },
})
}
同步 var x=();return
order-model.js
{
import { Base } from '../../utils/base.js';
class Order extends Base{
constructor(){
super();
this._storageKeyName='newOrder';
}
//下订单
doOrder(param,callBack){
var that=this;
var allParams={
url:'order',
type:'post',
data:{
products:param
},
sCallBack:function(data){
that.execSetStorageSync(true);
callBack&&callBack(data);
},
eCallBack:function(){}
};
this.request(allParams);
}
/**
* 拉起微信支付
* params:
* norderNumber-{int}订单id
* return:
* callback-{obj} 回调方法,返回参数
* 可能值0:商品缺货等原因导致订单不能支付;
* 1:支付失败或者支付取消
* 2:支付成功
*/
execPay(orderNumber,callBack){
var allParams={
url:'pay/pre_order',
type:'post',
data:{
id:orderNumber
},
sCallBack:function(data){
console.log('服务器返回:',data);
var timeStamp=data.timeStamp;//服务器返回的时间戳
if(timeStamp){//服务器返回的订单详细 存在 才支付
wx.requestPayment({//微信支付api
timeStamp: timeStamp.toString(),
nonceStr: data.nonceStr,
package: data.package,
signType: data.signType,
paySign: data.paySign,
success:function(){
callBack&&callBack(2);//更新订单支付状态 2支付成功
},
fail:function(){
callBack && callBack(1);//支付失败或者支付取消
//服务器 模拟返回可以在base==500 模拟
// 预防其他问题 测试完 改变
// 小程序 返回结果比较难 改状态 模拟 当无 过
// 思路
// callBack && callBack(2);
// 除非 反过来调试
// success 也是返回2 成功 或者反过来1
// 不用模拟成功失败
// order.js _execPay statusCode != 0 都执行
// fail = 1 success = 2 够用 可以不改 不是0就行了
// fail api本身就失败 测试失败当成功1变2 测试
}
})
}
else{
callBack && callBack(0);//更新订单支付状态 0没下单没支付
}
}
};
// console.log(allParams);
this.request(allParams);
}
//支付时 成功失败 同时请求服务器更改 支付状态
_toServiceStatus(status){
var params={
// api接口没编写 未知
}
this.request(params);
}
//获得订单的具体内容 服务器数据库中 订单快照
getOrderInfoById(id,callBack){
var that=this;
var allParams={
url:'order/'+id,
sCallBack:function(data){
callBack&&callBack(data);
},
eCallBack:function(){}
};
this.request(allParams);
}
//获得所有订单,pageIndex 从1开始 分页加载
getOrders(pageIndex,callBack){
var allParams={
url:'order/by_user',
data:{
page:pageIndex,
// size:15,
},
type:'get',//可省略 base.js
sCallBack: function (data) {
callBack && callBack(data);
},
eCallBack: function () { }
};
this.request(allParams);
}
//本地缓存 保存/更新
execSetStorageSync(data){
wx.setStorageSync(this._storageKeyName, data);//true 下单了
}
//是否有新的订单
hasNewOrder(){
var flag = wx.getStorageSync(this._storageKeyName);//下单了缓存true没false execSetStorageSync
return flag==true;
}
}
export{Order};
}
13-24弹出用户收货地址授权界面
wx.getSetting({ //经常用重复 可以提取
success: function (res) {
if (res.authSetting['scope.userInfo']) {
}
else {
wx.navigateTo({
url: '../authorize/authorize',
})
}
}
})
pay cart my限制了 可以不
theme home 限制了 可以不
要么设置全局处理
base.js ->page-model-page.js
清楚 缓存 权限 地址
点击 取消 fail wxapi
success fail 处理
wx.chooseAddress 地址 管理 同过
my
wxml
{
地址管理
+ 添加地址
}
js
{
// pages/my/my.js
import {My} from 'my-model.js';
import {Order} from '../order/order-model.js';
import {Address} from '../../utils/address.js';
var my=new My();
var order=new Order();
var address=new Address();
Page({
data: {
on:false,
pageIndex:1,//分页加载 加载1页15个default
orderArr:[],
isLoadedAll:false,//是否追加完
loadingHidden:false,//刚才加载时有点用
count:0,
login:false
},
editAddress:function(){
var that = this;
wx.chooseAddress({
success: function (res) {
console.log(res)
var addressInfo = {
name: res.userName,
mobile: res.telNumber,
totalDetail: address.setAddressInfo(res)
};
that._bindAddressInfo(addressInfo);
address.submitAddress(res, (flag) => {
if (!flag) {
that.showTips('操作提示', '地址信息更新失败!');
}
})
}
})
},
_bindAddressInfo: function (addressInfo) {
this.setData({
addressInfo: addressInfo
});
},
showTips: function (title, content, flag) {
wx.showModal({
title: title,
content: content,
showCancel: false,
success: function (res) {
if (flag) {
wx.switchTab({
url: '../my/my',
});
}
}
})
},
onLoad: function (options) {
// wx.openSetting({ 不行 未知
// success(res){
// console.log('openSetting',res)
// }
// })
// this._loadData();
// this._getAddressInfo();
my.getUserInfo((data) => {
this.setData({
userInfo: data
});
});
var that=this;
wx.getSetting({
success:function(res){
// console.log(res);
if (res.authSetting['scope.userInfo']){//onShow 返回可在判断 onLoad->authorize onshow->button
// that._loadData(); toShow重新onload
// that._loadData();
// that._getAddressInfo();
// that.setData({
// on: res.authSetting['scope.userInfo'],
// loadingHidden:true
// });
setTimeout(function () {
that._loadData();
that._getAddressInfo();
that.setData({
on: res.authSetting['scope.userInfo'],
loadingHidden:true,
login:true
});
}, 1000)
}
else{
wx.navigateTo({
url: '../authorize/authorize',
})
}
}
})
},
_getAddressInfo:function(){
//获得地址
address.getAddress((addressInfo)=>{
this._bindAddressInfo(addressInfo);
});
},
//绑定地址信息
_bindAddressInfo:function(addressInfo){
// this.data.addressInfo = addressInfo; 等价下 但新版刷新页面
this.setData({
addressInfo:addressInfo
});
},
_loadData:function(){
my.getUserInfo((data)=>{
this.setData({
userInfo:data
});
});
this._getOrders();
},
_getOrders:function(callBack){
order.getOrders(this.data.pageIndex,(res)=>{
if(Object.keys(res.data).length==0){
//res.data 服务器 wx版本
}
var data = res.data.data;
// if (data.length > 0)
// console.log(Object.keys(res.data).length!=0);
if (Object.keys(res.data).length != 0){
// for(let i=0;i=1){
// this.setData({ loadingHidden: true });//下面不跳转 又要btn true 但没加载效果 加累加器 存在时不执行
// }
wx.getSetting({ //经常用重复 可以提取
success: function (res) {
if ((!res.authSetting['scope.userInfo'])&&count >= 1) {
that.setData({ loadingHidden: true });
}
if (res.authSetting['scope.userInfo'] && (!that.data.login)) {
that._loadData()
that._getAddressInfo()
that.setData({
on: true,
login:true,
loadingHidden: true//这个没权限时不设置加载中 点击不了按钮 只能授权页面点击 好像合理不授权不加载 这样按钮多余 都行 备用 授权切换要用
});
}
else {//用button 不跳转
// wx.navigateTo({
// url: '../authorize/authorize',
// })
}
}
})
var newOrderStatus = order.hasNewOrder();//同步 var x=();return
if (newOrderStatus){
this.refresh();
}//直接加载 每次show加载 卡 消耗性能 orderModel 判断有新时才加载
},
refresh:function(){
//重新加载
var that=this;
this.data.orderArr=[];//订单初始化
this._getOrders(()=>{
console.log('refresh');//network测试
that.data.isLoadedAll=false;//是否加载完成
this.data.pageIndex=1;
order.execSetStorageSync(false);//更新标志位
});
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh: function () {
//解决 下单刷新 历史订单 刷新不自动返回-TODO->
// this.onLoad();//数组重复加载 除非onload data重新 过 onShow解决
this.onShow();//this.refresh
// this._getOrders();
console.log('下拉动作');//json字段声明appWin字段可能 page.json 全局
//json "enablePullDownRefresh": true
wx.stopPullDownRefresh();
//当处理完数据刷新后,wx.stopPullDownRefresh可以停止当前页面的下拉刷新。
// if2次停止计数器可以停止当前页面的下拉刷新。
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom: function () {
console.log('上拉触底');//json改底部距离 容易触发
// "onReachBottomDistance": 100 json
//当界面的下方距离页面底部距离小于100像素时触发回调
// this.setData({//追加打数组 重新显示
// pageIndex: this.data.pageIndex + 1//服务器 又加 限制 自己page+1 不行
// })
// this._getOrders();
if(!this.data.isLoadedAll){
//是否加载完成 false没加载完
this.data.pageIndex++;
this._getOrders();
}
else{
wx.showToast({
title: '已经全部显示历史订单',
})
}
// scroll - view 下拉功能
console.log('是否加载true完成:',this.data.isLoadedAll)
},
//显示订单的具体信息
showOrderDetailInfo:function(event){
var id=order.getDataSet(event,'id');//数据库标签传递来的订单号id
wx.navigateTo({
url: '../order/order?from=order&id='+id,
});
},
rePay:function(event){
var id = order.getDataSet(event,'id'),
index = order.getDataSet(event,'index');//orderArr下标
this._execPay(id,index);
},
_execPay:function(id,index){
var that=this;
order.execPay(id,(statusCode)=>{
console.log('状态:',statusCode);
statusCode=2;//测试成功2 1失败 0不足 临时赋值 刷新恢复 数据库没改状态
if(statusCode>0){
var flag=statusCode==2;
//更新订单显示状态
if(flag){
that.data.orderArr[index].status=2;
that.setData({
orderArr:that.data.orderArr
});
}
//跳转到 成功页面
wx.navigateTo({
url: '../pay-result/pay-result?id='+id+'&flag='+flag+'&from=my',
});
}
else{
that.showTips('支付失败','商品已下架或库存不足');
}
});
},
//提示窗口 params:title-{string}标题 content-{string}内容 flag-{bool}是否跳转到'我的页面'
// showTips:function(title,content){
// wx.showModal({
// title: title,
// content: content,
// showCancel:false,
// success:function(res){
// console.log(res.confirm,res.cancel);
// }
// })
// },
toShow:function(event){
// console.log(event)
//button open-type="getUserInfo" bindgetuserinfo="toShow"
//只能点击才能使用
// 客户端代码要改
// 不自动获得令牌
// 配套维持
// wx.login code -> servers -> oppleid -> sql -> token
// 不管 wx.getuserinfo用户信息事
// 大不了不登录 操作
// 但不够友好 可以操作下单 但是又没登录
// 解决 1.自动获得token移到登录授权 手动
// 2.自动token加条件授权才获取token
// 3.1 2配合使用 加载页面时出现引导 if授权 && -> token 再挑index
// && 一块 -> 先授权再token
if(event.detail.userInfo){
this.onLoad();
this.setData({on:true})
}
},
// token:function(event){
// console.log(event)
// wx.login({
// success:function(res){
// var code=res.code;
// wx.request({
// url: 'http://z.cn/api/v1/token/user',//get 在url
// data: {
// code: code//post data
// },
// method: 'POST',
// success:function(res){
// wx.setStorageSync('token', res.data.token);
// }
// })
// }
// })
// },
})
}
专题 到 product 过
theme
wxml
js
{
onProductsItemTap: function (event) {
// console.log(event)
var id = theme.getDataSet(event, 'id');
wx.navigateTo({
url: '../product/product?id=' + id,
})
},
}
D:\PanDownload\【微信小程序】\微信小程序商城构建全栈应用\
第14章 CMS与CORS跨域
多次打练 头到尾 先用过 重新头 重构 最好一次 不完全美过
14-1 如何理解CMS在架构中的位置_x264
cms 传统 h5 简单过 属html到时过
copy 源码 框架 html css js
zreg/public/cms
管理页面
教
cms 和 服务器api 交互 客户端
小程序 安卓 不存在跨域
php 服务器 跨域
zreg/public/cms
cms 和那个 Terran 同一个 可能
模板
数据库 商品 product
cms 添加 删除
cms 不是 和数据库直连
cms 自己 连接服务器api 连接 数据库
普通 的客户端 调用 后台cms 就是html 单页面应用程序
开源 cms 源码 扩展 框架 ->
dataBase
redis
restapi token 业务逻辑
客户端 网站 ios android 小程序 h5 第三方应用
uni-app ionic flutter 混合开发
百度 京东 dcloud 开源技术
网站 传统网站
api后端->服务器不处理业务逻辑仅渲染前端->html前端 复杂时 传统
仅html->单页面
以前和在一块 现在分离 三段分离
14-2 访问CMS
http://z.cn/cms/pages/login.html
php开发后台 其他 也可以 -->麻烦
js 框架 ui css
纯 js 写 过
js jq具本工具
能自己写 尽量jsjq写 框架不一定 好用
cms 网站
cms/js/common.js
url=服务器api地址
g_restUrl:'REST API Base URL',//相对路径 传统网站
g_restUrl:'http://z.cn/api/v1/',放到其他地方 网址一样访问
cms api 编写
三段分离
cms 到时 独立出去
暂时 public/cms 传统一样
14-3 应用令牌获取接口与应用令牌的理解
Storage -sessionlocal
....
cookies
cache应用
login
模块不存在:cms
v1/token geToken是小程序的登录接口
需要 传统 的 账号 密码 又点不同
传统 是用 cookies storage 管理
cms cache cookies ...
token cache cms的token 令牌
可以其他 注册 调用 第三方
不仅仅
对应 model 数据表
third_app 第三方 信息
app_id=uid帐号 app_secret密码 scope权限
first second third
注册 增加帐号密码 默认权限 申请等等 页面 接口 重构
权限 查看 增加 禁止
自己 有空 增加 简单 uid 测试 到时
数据库 保存时 帐号密码 不要 直接明文
md5 ... 加密
查看 解密 读取时
RDS 子权限
权限控制
RDS数据库子账号权限设置
可以控制 那个账号 可以访问 那个页面
api给用户 是 针对 cms 颁发 权限
权限 在进行 细分
站后 架构
总权限 最高的 再 细分
html-api-sql-token有效-返回token
app_description 表示 是那个平台 cms
浏览器 缓存 地址改变
退出看看
V1/token
{
/**
* 第三方应用获取令牌 cms 其他平台
* @url /app_token?
* @POST ac=:ac帐号 se=:secret密码
*/
public function getAppToken($ac='',$se=''){
(new AppTokenGet())->goCheck();//validate
$app=new AppToken();//service
$token=$app->get($ac,$se);
return [
'token'=>$token
];
}
}
route
{
Route::post('api/:version/token/app','api/:version.Token/getAppToken');
}
Service
{
'授权失败',
'errorCode'=>10004
]);
}
else{
$scope=$app->scope;//权限 管理员...
$uid=$app->id;//保存统一 app_id
$values=[
'scope'=>$scope,
'uid'=>$uid
];
$token=$this->saveToCache($values);
return $token;
}
}
private function saveToCache($values){
//保存 用户权限和uid 到 tp 缓存
$token=self::generateToken();//Token父类里的方法 生成随机的Token
$expire_in=config('setting.token_expire_in');//到期时间 3天
$result=cache($token,json_encode($values),$expire_in);
//保存缓存 Token Array-json(scope,uid) 到期时间
if(!$result){
throw new TokenException([
'mag'=>'服务器缓存异常',
'errorCode'=>10005
]);
}
return $token;
}
}
}
model
{
where('app_secret','=',$se)
->find();
return $app;
}
}
}
validate
{
'require|isNotEmpty',//帐号密码不能为空
'se'=>'require|isNotEmpty'
];
}
}
14-4 获取所有订单(分页)
二次 支付 历史订单
服务器 会自动 再 库存量检测
不管客户端时
服务器 加载 可以 没点
测试 自己改 我的点击 支付 可以成功
进入历史订单 是 服务器加载 服务器是未支付 才可以点击
取消测试 全部正常
order.js
{
_oneMoresTimePay:function(){
if (this.data.orderStatus == 1) {
//订单号存在id 下单状态1orderStatus
console.log('2:'+this.data.id);
this._execPay(this.data.id);
}
},
}
my.js
_execPay
//statusCode=2;//测试成功2 1失败 0不足 临时赋值 刷新恢复 数据库没改状态
order.php
所有历史订单 全部用户
显示
getSummaryByUser
用户的
getSummary
cms 全部用户
版本
res.data.data index.js cms 类似小程序
html
http://z.cn/api/v1/order/paginate?XDEBUG_SESSION_START=13105
css js ui不次要
服务器 逻辑 重要 义务逻辑 架构
加载更多 有点问题
发货接口 没改
自己diy
数组 空 改 else
route
{
Route::get('api/:version/order/paginate','api/:version.Order/getSummary');
}
v1/order
{
/**
* 获取全订单简要信息(分页)
* @param int $page
* @param int $size
* @return array
* @throws \app\lib\exception\ParameterException
*/
public function getSummary($page=1,$size=20){
(new PagingParameter())->goCheck();
$pagingOrders=\app\api\model\Order::getSummaryByPage($page,$size);
if ($pagingOrders->isEmpty()){
return [
'current_page'=>$pagingOrders->currentPage(),
'data'=>[]
];
}
$data=$pagingOrders->hidden(['snap_items','snap_address'])
->toArray();
return [
'current_page'=>$pagingOrders->currentPage(),
'data'=>$data
];
}
}
model/order
{
public static function getSummaryByPage($page=1,$size=20){
$pagingData=self::order('create_time','desc')
->paginate($size,true,['page'=>$page]);
return $pagingData;
}
}
14-5 微信模板消息介绍
微信模板消息
被动 一定范围 才 返回
机制 不直接
https://mp.weixin.qq.com/debug/wxadoc/dev/api/notice.html#接口说明
下发条件说明
新版无
下发条件说明
1,支付
当用户在小程序内完成过支付行为,可允许开发者向用户在7天内推送有限条数的模板消意(1次支付可下发3条,多次支付下发条数独立,互相不影响)
2,提交表单
当用户在小程序内发生过提交表单行为且该表单声明为要发模板消息的,开发者需要向用户提供服务时,可允许开发者向用户在7天内推送有限条数的模板消息(1次提交表单可下发1条,多次提交下发条教独立,相互不影响)
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/template-message.html
支付 通知 模板
请注意,小程序模板消息接口将于2020年1月10日下线,开发者可使用订阅消息功能
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/subscribe-message.html
https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/template-message/templateMessage.addTemplate.html
templateMessage.addTemplate
需要pre_id
没企业过 旧版 新改无 过-
使用说明
1.获取模板id登录https://mp.weixin.qq.com获取模板 如果有合s的e板0以申请添加新模板,审核通过后可使用,详见模板审核说明
订单发送成功 的模板
支付不行 模板 更不 过
模板 是微信api的 调用wxapi
appid appsecret 调用接口 获得 access_token
POST https://api.weixin.qq.com/cgi-bin/wxopen/template/add?access_token=ACCESS_TOKEN
https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/template-message/templateMessage.addTemplate.html
最新用
小程序订阅消息功能介绍
支付 模板 订阅信息未知不稳 不行 公告号到时
过
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/template-message.html
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/subscribe-message.html
14-6 实现发送模板消息
学习 文档 看看
service
DeliveryMessage.php
模板 xx 过
子类 拼凑
创建一个基类 WxMessage 发送到wxapi
appid acc_token ...
选级 分小
地址
https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/template-message/templateMessage.send.html
POST https://api.weixin.qq.com/cgi-bin/message/wxopen/template/send?access_token=ACCESS_TOKEN
extra/wx.php
//微信获取access_token的url地址
'access_token_url' => "https://api.weixin.qq.com/cgi-bin/token?" .
"grant_type=client_credential&appid=%s&secret=%s",
Service
AsscessToken.php
{
tokenUrl=$url;
}
//建议用户规模小时每次直接去微信服务器取最新的token
//但微信access_token接口获取是有限制的2000次/天
public function get(){
$token=$this->getFromCache();//获得缓存 token
if (!$token){
return $this->getFromWxServer();//没有请求服务器 得 token
}
else{
return $token;
}
}
private function getFromCache(){
$token=cache(self::TOKEN_CACHED_KEY);//读取缓存存不
if (!$token){
return $token;
}
return null;
}
private function getFromWxServer(){
$token=curl_get($this->tokenUrl);
$token=json_decode($token,true);
if (!$token){
throw new Exception('获取AccessToken异常');
}
if (!empty($token['errcode'])){
throw new Exception($token['errmsg']);
}
$this->saveToCache($token);
return $token['access_token'];
}
private function saveToCache($token){
}
}
}
WxMessage.php
{
get();
$this->sendUrl=sprintf($this->sendUrl,$token);
}
}
}
DeliveryMessage.php
{
tplID=self::DELIVERY_MSG_ID;
$this->formID=$order->prepay_id;
$this->page=$tplJumpPage;
$this->prepareMessageData($order);
$this->emphasisKeyWord='keyword2.DATA';
return parent::sendMessage($this->getUserOpenID($order->user_id));
}
private function prepareMessageData($order){
$dt=new \DateTime();//\php wxapi
$data=[//模板要求
'keyword1'=>[
'value'=>'顺风速运',
],
'keyword2'=>[
'value'=>$order->snap_name,
'color'=>'#27408B'
],
'keyword3'=>[
'value'=>$order->order_no
],
'keyword4'=>[
'value'=>$dt->format("Y-m-d H:i")
]
];
$this->data=$data;
}
}
}
14-7 测试发送模板消息
拆3类 好操作
WxMessage基类 不变业务放基类
DeliveryMessage 其中一个模板消息类 多个模板多个类
开闭原则 扩展
AccessToken类获得AccessToken 独立 其他调用呢
prepay_id 开发工具的不行 真机
不全 过
以前 没 过 demo过
cms 不过 成功不 通知 了 改 发货了
v1/Order
{
protected $beforeActionList=[//前置操作
'checkExclusiveScope'=>['only'=>'placeorder'],//要小写createOrUpdateAddress
'checkPrimaryScope'=>['only'=>'getdetail,getsummarybyuser'],//权限控制 用户管理员都可以访问 历史订单
'checkSuperScope' => ['only' => 'delivery,getsummary'] //仅cms 管理员
];
public function delivery($id){
//$id 订单号id
(new IDMustBePostiveInt())->goCheck();
$order=new OrderService();
$success=$order->delivery($id);
if ($success){
return new SuccessMessage();
}
}
}
Service/order
{
public function delivery($orderID,$jumpPage=""){
$order=\app\api\model\Order::where('id','=',$orderID)->find();
if (!$order){
throw new OrderException();
}
if ($order->status!=OrderStatusEnum::PAID){
throw new OrderException([//没支付 已经发货
'msg'=>'还没付款呢,想干嘛?或者你已经更新过订单了,不要再刷了',
'errorCode'=>80002,
'code'=>403
]);
}
$order->status=OrderStatusEnum::DELIVERED;
$order->save();
// ->update(['status'=>OrderStatusEnum::DELIVERED]);
$message=new DeliveryMessage();
return $message->sendDeliveryMessage($order,$jumpPage);//微信模板发送 微信通知用户
}
}
}
v1/BaseController
{
protected function checkSuperScope()
{
TokenService::needSuperScope();
}
}
Service/Token
{
//只有用户才能访问的接口权限
public static function needExclusiveScope(){
$scope=self::getCurrentTokenVar('scope');
if ($scope){//是否存在
if ($scope==ScopeEnum::User){//权限=16
return true;
}else{
throw new ForbiddenException();//抛出异常中断后面操作
}
}
else{
throw new TokenException();//Token已过期或无效Token
}
}
}
route
Route::put('api/:version/order/delivery','api/:version.Order/delivery')
更新 put
common.php
{
function curl_post($url, array $params = array())
{
$data_string = json_encode($params);
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
curl_setopt(
$ch, CURLOPT_HTTPHEADER,
array(
'Content-Type: application/json'
)
);
$data = curl_exec($ch);
curl_close($ch);
return ($data);
}
}
Service/WxMessage
{
get();
$this->sendUrl=sprintf($this->sendUrl,$token);
}
////开发工具中拉起的微信支付prepay_id是无效的,需要在真机上拉起支付
protected function sendMessage($openID){
$data=[
'touser'=>$openID,
'template_id'=>$this->tplID,
'page'=>$this->page,
'form_id'=>$this->data,
// 'color'=>$this->color,
'emphasis_keyword'=>$this->emphasisKeyWord
];
$result=curl_post($this->sendUrl,$data);
$result=json_decode($result,true);
if ($result['errcode']==0){
return true;
}
else{
throw new Exception('模板消息发送失败, '.$result['errmsg']);
}
}
}
}
Service/AsscessToken
{
tokenUrl=$url;
}
//建议用户规模小时每次直接去微信服务器取最新的token
//但微信access_token接口获取是有限制的2000次/天
public function get(){
$token=$this->getFromCache();//获得缓存 token
if (!$token){
return $this->getFromWxServer();//没有请求服务器 得 token
}
else{
return $token;
}
}
private function getFromCache(){
$token=cache(self::TOKEN_CACHED_KEY);//读取缓存存不
if (!$token){
return $token;
}
return null;
}
private function getFromWxServer(){
$token=curl_get($this->tokenUrl);
$token=json_decode($token,true);
if (!$token){
throw new Exception('获取AccessToken异常');
}
if (!empty($token['errcode'])){
throw new Exception($token['errmsg']);
}
$this->saveToCache($token);
return $token['access_token'];
}
private function saveToCache($token){
cache(self::TOKEN_CACHED_KEY, $token, self::TOKEN_EXPIRE_IN);
}
}
}
https://mp.weixin.qq.com/wxopen/tmplmsg?action=self_list&token=603864058&lang=zh_CN
由于“模板消息”将下线,已不再支持添加模板,请尽快接入“订阅消息”。
模板不能添加了 过
小程序订阅消息开发文档 到时 过
过
Service/DeliveryMessage
{
tplID=self::DELIVERY_MSG_ID;
$this->formID=$order->prepay_id;
$this->page=$tplJumpPage;
$this->prepareMessageData($order);
$this->emphasisKeyWord='keyword2.DATA';
return parent::sendMessage($this->getUserOpenID($order->user_id));
}
private function prepareMessageData($order){
$dt=new \DateTime();//\php wxapi
$data=[//模板要求
'keyword1'=>[
'value'=>'顺风速运',
],
'keyword2'=>[
'value'=>$order->snap_name,
'color'=>'#27408B'
],
'keyword3'=>[
'value'=>$order->order_no
],
'keyword4'=>[
'value'=>$dt->format("Y-m-d H:i")
]
];
$this->data=$data;
}
private function getUserOpenID($uid){
$user=User::get($uid);
if (!$user){
throw new UserException();
}
return $user->openid;
}
}
}
到时自己看文档 弄
测试
模板消息发送失败, invalid template_id hint: [oWKpJA01392066]
不行 过 到时
刷新 发货了
数据库改了 加载
配套视频-
管那微信通不通知
发货 就 改状态
14-8 分离CMS
分销cms tpshop ...注意后台
cms 开源 框架 ...
订单列表
左边
框架 添加 需要 的主题 商品等
分离CMS
要跨域问题
和zerg同级 目录
cms->=terran
配置域名
host phpstudy域名 apache加
phpstudy 加 自动 加 apachehost
t.cn
C:\phpStudy\WWW\terran
DocumentRoot "C:\phpStudy\WWW\terran"
ServerName www.t.cn
ServerAlias t.cn
Options FollowSymLinks ExecCGI
AllowOverride All
Order allow,deny
Allow from all
Require all granted
127.0.0.1 t.cn
127.0.0.1 www.t.cn
t.cn/pages/login.html
不能直接 登录 跨域问题
Access to XMLHttpRequest at
'http://z.cn/api/v1/token/app' from
origin 'http://www.t.cn' has been blocked
by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
脚本 不同 一个域名下 不能直接
14-9 CORS跨域的概念与TP5的解决方案
js php 跨域问题
小程序同一个域名 暂时不
访问加 getAppToken
v1/token
{
public function getAppToken($ac='',$se=''){
//跨域问题
header('Access-Control-Allow-Origin: *');
所有域名访问 api
header("Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept");
应许访问时 携带那些键值对
header('Access-Control-Allow-Methods: POST,GET');
(new AppTokenGet())->goCheck();//validate
$app=new AppToken();//service
$token=$app->get($ac,$se);
return [
'token'=>$token
];
}
}
Order/v1
delivery
都加
getSummary
General 控制台 network headers
Request URL: http://z.cn/api/v1/order/paginate?page=1&size=20
Request Method: OPTIONS
cms-get-option请求-api-accept字段有
再真正get ->服务器 响应
服务器 没有 option请求
不能跨域 找不到
服务器 route 支持 option请求 get post
小型项目 还可以
大型 多改 基类
aop 思想 切面 看 基类 加
不是所有浏览器 都 option请求 chrome是
简单 不发
复杂 发
CORS简单请求 search
随便网上
www.pooren.com/cross-orgin-resource-sharing-complex
跨域请求 aop 全局处理
发送请求 全局拦截 是option什么放行 其他返回
cors 全局 处理
tp 自带 默认行为
tp/application/tags.php
init 开始应用 调用一些方法
aop切面 切入
创建api/behavior/cors.php
行为
cms
测试 登录 和 获得订单详细
布局 自动适应
已经全局处理aop tags.php->behavior-cors
{
isOptions()){
exit();//助手 请求是options退出
}
}
}
}
{
// +----------------------------------------------------------------------
// 应用行为扩展定义文件
return [
// 应用初始化
'app_init' => [
'app\\api\\behavior\\CORS' //命名空间 调用行为 \\预防转义
],
// 应用开始
'app_begin' => [],
// 模块初始化
'module_init' => [],
// 操作开始执行
'action_begin' => [],
// 视图内容过滤
'view_filter' => [],
// 日志写入
'log_write' => [],
// 应用结束
'app_end' => [],
];
}
chrome 找不到 才不行
服务器 禁止它 就行了 响应 chrome 相当 回应
chrome 才继续 真的 get post 请求 跨域 才这样
api tp 支持跨域
小程序没事
单网页 网站 就要 支持跨域
下节 生成数据库表字段与路由缓存提升性能
cms分离和不 都行了
小程序 行
服务器 行
问题
商品详情 购物车动态 数目
显示数目 如果是显示 总数目 是对 过 配套
如果是单个数目 不对 麻烦 过
商品详情 购物车动态icon css
源码 提示 找到会 前视频后 修bug xinjiu
购物车动态 点击购物车动态 位置样式 多点 下拉 分享
分类 动态 css
不是在page 在模板 加
不知道怎么移动 过 css到时系统 交换 ui 到时 过
测试 少 应许 token了
D:\PanDownload\【微信小程序】\微信小程序商城构建全栈应用\
第15章 小程序部署流程与TP5在生产环境中的优化
15-1 生成数据库表字段与路由缓存提升性能
{{(categoryInfo.index-1==-1)?'':'tanslate'+(categoryInfo.index-1)}}
.tanslate0{
transform:translate(0,0);
}
移动 不行 不会 过
不教过 不会 过
不看教程源码
自己 模仿 再多写
模仿写 多写
项目 连续 真实
用户压力 环境不同 情况不同 决定 优化
config dubug 关闭
model 数据库缓存
banner 数据库 字段有没有
tp 先sql查询都有什么表什么字段 查出
tp 先sql查询都有什么表什么字段 查出
日子记录路
[sql] show columns from `user` runteime.xxx
多了 性能 差
把字段 缓存 起来 差 没在sql
命令行
php think optimize:schema
sql字段 缓存 在runtime/schema
所有表 对应 表class
路由 缓存
多用户访问 多查数据库 性能差
cmd
php think optimize:route
缓存 在
runtime/route.php
缓存 分 很多种 优化
数据库缓存
路由缓存 中大型项目 业务层
CDN缓存
前端浏览器 缓存
15-2 另一种思路处理库存量的问题
发布 流程 看文档
没教过
不全过
第一种
//下单接口 下单数据增加记录 复杂 库存量 支付
//1.用户在选择商品后,向AP1提交包含它所选择商品的相关信息
//2.APT在接收到信息后,需要检查订单相关商品的库存量
//3.有库存,把订单数据存入数据库中:下单成功了,返回客户端消息,告诉客户端可以支付了
//4.调用我们的支付接口,进行支付
//5.还需要再次进行库存量检测
//6.服务器这边就可以调用微信的支付接口进行支付
// 小程序根据服务器返回的结果拉起微信支付
//7.微信会返回给我们一个支付的结果(异步) 俩个途径异步服务端 同步客户端
//8.成功:也需要进行库存量的检查
//9.成功:进行库存量的扣除,[微信异步 失败:返回一个支付失败的结果]
第二种 网上查其他->
//别的方法处理库存量
//做一次库存量检测
//创建订单
//减库存--预扣除库存 未支付
//if pay 正真的减库存
//在一定时间内30min没有去支付这个订单,我们需要还原库存 成本高 难还原
//在php里写一个定时器,每隔1min去遍历数据库,找到那些超时的
//订单,把这些订单给还原库存
//liunx crontab 定时
//任务队列
//订单任务加入任务队列里
//redis other实时
//定时器,每隔1s,5s,访问api的接口 客户端 循环
CRMEB 二三级分销 多级-
二三级分销-crmeb包括分销-
不全过 - 包括全
tpshop-
tp5微信开发(六)--- tp5微信三级分销关系建立;微信jssdk分享 - 优倍素材网技术总结 - CSDN博客
https://blog.csdn.net/qq_27987023/article/details/89810104
使用thinkphp搞了一个简单的三级分销 - qq_20964509的博客 - CSDN博客
https://blog.csdn.net/qq_20964509/article/details/84341438
ctrl+2点
https://gitee.com/ZhongBangKeJi/CRMEB
微信公众号商城、小程序商城、H5商城数据同步,带积分、优惠券、秒杀、会员等级、分销等功能,前后端全部开源,更是一套方便二次开发的框架。
v3.0采用TP6框架。官网地址:http://www.crmeb.com
CRMEB商城系统是基于ThinkPhp6.0+Vue开发的一套新零售移动电商系统,CRMEB系统就是集客户关系管理+营销电商系统,能够快速积累客户、会员数据分析、智能转化客户、 有效提高销售、会员维护、网络营销的一款企业应用,包含商城、拼团、砍价、秒杀、优惠券、积分、分销等功能,更适合企业二次开发;
学过|旧挑重
https://gitee.com/ZhongBangKeJi/CRMEB
第一步是先思考下逻辑
分销就是成员与成员的层级关系
写评论功能的时候评论人和回复人的关系差不多吧
Distribution
distribution
1.先建个表吧,用来存放层级关系
网上
2.找到个人用户中心,直接用后台管理员的用户就可以登上去了。至于前端页面什么的就不写了。。
3.直接上代码吧
\app\user\controller\DistributeController.php
没有数据过 没添加 过
最新3.x
本地 v2.3.*
https://help.crmeb.net/
https://help.crmeb.net/crmeb/662420
基于ThinkPhp5.0+Vue+EasyWeChat 开发的一套CRMEB新零售商城系统
安装程序会自动执行安装。期间系统会提醒你输入数据库信息以完成安装,安装完成后建议删除install目录下index.php文件或将其改名。
zerg 5.5.38
crmeb 请升级到5.5.9或更高版本再安装,谢谢!
上传你的代码,直接在浏览器中输入你的域名或IP(例如:www.yourdomain.com),
crmeb/public
www.c.cn phpstudy auto apache
host
一键安装
为了您站点的安全,
安装完成后即可将网站根目录下的“install”文件夹删除,
或者/install/目录下创建install.lock文件防止重复安装。
后台访问地址:
1.域名/admin
2.域名/index.php/admin
3.域名/index.php?s=/admin
公众号首页访问地址:
1.域名/wap
2.域名/index.php/wap
3.域名/index.php?s=/wap
重新安装
清除数据库
删除/public/install/install.lock 文件
手动安装
https
https://www.jianshu.com/p/eaad77ed1c1b
当前版本为:CRMEB-DTKY v2.5.36b
.htaccess
Options +FollowSymlinks -Multiviews
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-d
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ index.php?/$1 [QSA,PT,L]
v2.5 v2.6 v3.0 3.x-
v2.6-tp5 v3.0-tp6
v2.6
v2.6.13-
https://gitee.com/ZhongBangKeJi/CRMEB/repository/archive/v2.6.13
https://github.com/crmeb/CRMEB/tree/v2.6.13
https://codeload.github.com/crmeb/CRMEB/zip/v2.6.13
git 加速
https://blog.csdn.net/mist99/article/details/80602090
ipconfig /flushdns
pclond
other pan
原来的访问URL:
http://serverName/index.php/模块/控制器/操作/[参数名/参数值...]
设置后,我们可以采用下面的方式访问:
http://serverName/模块/控制器/操作/[参数名/参数值...]
phpstudy8.1
https://www.xp.cn/wenda/401.html
3.x-
3.1.2服务端...
https://codeload.github.com/crmeb/CRMEB/zip/master
小程序单独
https://gitee.com/ZhongBangKeJi/CRMEB_WechatApplet
sql_mode
请在mysql配置文件修sql-mode为NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
my.ini
sql-mode = NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
sql-mode或sql_mode
https://www.cnblogs.com/Zender/p/8270833.html
重启 mysql
https://www.c.cn/index.php/admin/login/index
https://www.c.cn/index.php 公众号
https://www.c.cn/
tp6
https://www.kancloud.cn/manual/thinkphp6_0/content
https://help.crmeb.net/crmeb_30/1277257
全部旧2.6
https://gitee.com/ZhongBangKeJi/CRMEB_WeChatMiniProgram
http://bbs.crmeb.net/thread-1667-1-1.html
二次开发系列视频教程,已增加APP封装
https://ke.qq.com/course/package/21476
盘 www 各个 二次开发
https://www.dcloud.io/index.html
hbuilderX
https://blog.csdn.net/qq_36478297/article/details/88181997
官方IDE。建议下载App开发版,如下载标准版,还需在插件管理中安装uni-app插件。
v2.4.6
善于搜集精美的小组件: “我们不生产代码,我们只是代码的搬运工”,善于找到想要的组件并把他们巧妙优雅的组装成一个大项目,也算是程序员一项基本技能了。
http://bbs.crmeb.net/thread-1667-1-1.html
收费 模板
https://ke.qq.com/course/package/21476
可能https 问题
分 开启动 数据库
俩phpstudy
es_weichat_user-openid
es_user 自己的id
服务端新-新 client旧不行 newsql
phpstudy8.1
新sql
https
服务端olds-oldc client新的不行 newsql
phpstudy8.1
新sql
https
服务端olds newsql
必须https phpstudy2016不行 8.1才行
-oldc 可以 页面显示授权可以
newC 不行 不显示 授权更不
不管new
old 的 php高 sql兼容 https固定
测试
同a|php|https 不同数据库 oldsql
oldServer
newCError
oldC-用户信息不显示
newServer oldSql cms位置变化 sql问题
newC 不兼容oldsql 页面显示授权不行
oldC oldSql client旧不行授权不行
方法一 ok
升Server 保oldSql必须加字段 升Client
保oldSql必须加字段 否则页面显示授权不行
升级系统
方法二
oldServer改oldClient 授权 信息显示
方法三 error
服务端olds newsql(保oldSql必须加字段)
-oldc 可以 页面显示授权可以
方法四 ok
newServer newsql(保oldSql必须加字段)
newC 改url 去官方
官网工具
暂时 用 方法一=四
c可以^ sql^ s^
scms可管理
新增 img url有问题 sql没事 可能httpsphpstudy工具问题
httpsphpstudy工具问题 设置http->https 工具路径有问题url
服务器phpapi问题少 其他都没事官网 改存数据库是 都保存为https 或者正确设置https
客户端改 替换https
http->https 跳转 少/
工具问题 不管scq
改sqlhttps 不知怎么改工具 过
测试二
尽量不改动
VM1782 asdebug.js:1 POST http://www.c.cn/ebapi/login/index.html 原serversql 不同phpstudyphpapache
https问题
无数组 不知道在那题 可提现
$this->userInfo['extractPrice'] = $this->userInfo['brokerage_price']=1;//可提现
http://www.c.cn/admin/store.store_product/save.html x
http://www.c.cn\uploads\config/image\s_5dfcc3b33aa35.jpg
C:\phpstudy_pro\WWW\CRMEB\crmeb\public\uploads\attach\2019\12\20191220
路由不知 批量导出行 但入不 数据库加载
测试三
Snew Cnew 模拟升数据库
图片路径 批量sql替换
购物商品里面 问题
微信用户信息授权出问题
服务器 客户端 版本问题
方法二,进入 MySQL 控制台(如:MySQL 5.5 Command Line Client),使用 source 命令执行
Mysql>source 【sql脚本文件的路径全名】 或 Mysql>\. 【sql脚本文件的路径全名】,示例:
source C:\test.sql 或者 \. C:\test.sql
打开 MySQL Command Line Client,输入数据库密码进行登录,然后使用 source 命令或者 \.
方法一,在 Windows 下使用 cmd 命令执行(或 Unix 或 Linux 控制台下)
【Mysql的bin目录】\mysql –u用户名 –p密码 –D数据库<【sql脚本文件路径全名】,示例:
C:\MySQL\bin\mysql –uroot –p123456 -Dtest
https://coding.imooc.com/class/251.html?mc_marking=bea1182dc0a4f4e99b7531fb2e143357&mc_channel=shouji
《全面系统 Python3入门+进阶课程》
《Python Flask高级编程》
《Python Flask构建可扩展的RESTful API》
手记:
《编程之路 ——给初学者的学习建议》
《16.12.21小程序0.11.122100版本更新问题》
《17年1月9日,小程序来了。深度解析2017微信公开课》
《小程序来了!!!》
《来自学员的两款小程序。开发者,你应当拥有一款自己的产品。》
《程序员,你真的会写简历吗?》
《新课程预览:TP5.0+小程序+微信支付构建全栈应用》
《REST与RESTFul API最佳实践》
http://www.imooc.com/article/17650
《追寻本质还是流于形式》
《给编程初学者的一些建议》
《再看微信小程序》
https://www.jianshu.com/p/ed7769d32f37
笔记
https://blog.csdn.net/chinalihua/article/category/7273029
https://juejin.im/post/5c85c07df265da2dd94ce0bf#heading-4
https://yhf7.github.io/2019/02/18/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8Fphp%E5%90%8E%E7%AB%AF%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
主要
https://cloud.tencent.com/developer/article/1335760
https://blog.csdn.net/chinalihua/article/category/7273029
https://juejin.im/post/5c85c07df265da2dd94ce0bf#heading-28
https://www.jianshu.com/p/5a402cdae552
140 2 70 7 20
->=7->
步骤:
//1.控制器 方法定义完去route.php 定义路由
//2.如果客户端传入参数
//3.参数校验.
//4.编写模型 关联关系 封装with模型关系
//5.手动将模型引入进来 use app\api\model\Product as ProductModel;
//6.编写异常 Exception.php
//7.ok
search 显示行号 show line
切换代码 alt <- ->
查找文件,
ctril+shift+n所有文件 ctrl+E最近文件 ctrl+shift+E最近本地文件
set recent files 汉化没
ctrl+E
输入查看的东西
行注释ctrl+/
块注释ctrl+shift+/
alt+/ autoFill
搜索
全局ctrl+shift+f
局部ctrl+f
替换
全局ctrl+shift+r
局部ctrl+r
ALT+F1 选择当前文件或菜单中的任何视图工具栏
新建文件
目录focus
alt+insert
最左边
Structure
快速定位方法...
方法切换
alt+上下
--dug
Ctrl +反引号(`) 快速切换目前的配色/代码方案/快捷键方案/界面方案
alt+tab win
ctrl+tab 可以左右文件 和文件夹
win10如何在microsoft edge上保存网页为PDF
https://jingyan.baidu.com/article/7908e85c9cfb27af491ad25e.html
别人的笔记
配套网页版
https://www.jianshu.com/p/4940e1881c02?utm_campaign=maleskine
https://www.jianshu.com/p/5a402cdae552
外键约束:其实一直是业内人员经常讨论的一个话题 到底使用不使用外键约束
不使用的原因是因为业务有的大的项目 业务需要几乎2周就要更新一次
-->
https://developers.weixin.qq.com/miniprogram/dev/framework/
配套-tp全 123分轮
https://blog.csdn.net/E_li_na/article/details/87873082
小程序VM144:1 request:fail url not in domain list
{
可能原因:
域名未完成备案
url里有端口(可以有端口存在)
报错提示说请求的url不在域名列表里,应该是还没有配置服务器域名,可点击开发者工具右上角 详情-域名信息,看看是否配置了域名;
详情->项目设置->不校验合法域名
}
微信小程序-消息提示框实例
https://blog.csdn.net/zgmu/article/details/53435010
wx.showToast({
title: '成功',
icon: 'success',
duration: 2000
})
https://blog.csdn.net/Jensen_Yao/article/details/79475431
Toast
toast:function(){
var vm=this;
wx.showToast({
title: '提示框',
icon: 'success',
duration: 2000
})
//函数内调用不用注册app.json地址
wx.getLocation({
success: function(res) {
var longitude=res.longitude;
var latitude=res.latitude;
vm.setData({
longitude:longitude,
latitude:latitude
})
},
})
},
WXML->HTML
div view
h1 text
i icon
input
inputTypeFile
a href
imgSrc
navigate导航
redirect 重定向
https://blog.csdn.net/tianxintiandisheng/article/details/82464324
https://zhuanlan.zhihu.com/p/23536784
https://jingyan.baidu.com/article/6d704a135742b428db51ca17.html
navigate url="../fm/index"
在app.json 声明否则访问不了
"pages/fm/index"
navigate + open-type="navigate" 左上角又返回键
select option 支持
picker 时间选择
shift+{} 自动{中}
==
https://developers.weixin.qq.com/miniprogram/dev/framework/structure.html
地图定位简单
思路:
https://blog.csdn.net/weixin_34194702/article/details/91455581
wxml
{
}
wxjs
{
page({
data:{
longitude:null,
latitude: null,
},
onLoad:function(){
const vm=this;
wx.getLocation({
success:(res)=>{
var y=res.latitude;
var x=res.longitude;
vm.setData({ longitude: x, latitude: y});
}
})
}
})
}
app.json+
{
"permission":{
"scope.userLocation":{
"desc":"Please enter"
}
},
}
轻松玩转微信小程序开发
智能社
小程序:
---
网站:https://mp.weixin.qq.com/
注册:
个人用户(认证--不靠谱)
小程序->没法发布
AppID(小程序ID)
可以功能多一些
*手机游览 预览
不注册:
大部分功能可用
*不可预览
---------
开发者工具
下一步
同样可以使用:sublime,webstorm
-----
Chrome手机看
下载微信官方提供源码->相同vue脚手架
现成目录机构
-----
无Appid->无法真机(手机)预览
有AppId->可以预览
-----
如果使用开发者工具?
1.编写代码
2.调试
----
如果vue\angular\react,转到小程序->秒转
ng-xx v-xxx
-----
之前:
html
div
p
img
button
css
样式
js
现在:
wxml:微信里面标记
wxss:微信里面css,跟咱之前css一样
js:也差不多
------
pages->每个小程序页面
app.js
app.json
app.wxss
-整个小程序公共文件
-----
wx
window.alert window 不能用
在微信里面window没法用
不能bindtap="a(n)" js a(n){}
----
wx 全局对象
----
添加事件:
bindtap:
------
切换:
1.tourl
i
2.wx.navigateTo({
url:'../url/',
成功 失败 都 干什么
})
vue框架 cli
----
json最少是空{} 否则error
console wxmlhtml代码element save--.
AppData:数据data
放大ctrl-alt-+
souces
wx.setStorage({key:'name'})
wx.clearStorage();
this.data.motto
可以访问
但不能直接
this.data.motto="aaa"
要
this.setData({motto:''})
Date.now()
现在时间
navigateTo 设置了tabbar的不能在跳转
App与Page,wx与windows,添加事件,切换页面等
每个页面json 设置不同的title
最好颜色不要 red单词 #000简写
部分手机不太友好 最后#000000
-----
App()->注册一个小程序
Page()->注册一个页面
-----
模块化:
导出模块:
module.exports.x=x
exports.x=x
module.exports={};
使用:
let com=require(模块路径);
{
function echoHello(user){
return `欢迎用户${user}回来`;
}
function echoBye(user){
return `${user}退出,下次再来`;
}
module.exports.echoHello=echoHello;
exports.echoBye=echoBye;
}
---
更改数据同步view:
this.setData({})
wx:if="{{bool}}" iftruefalse
显示|不
!boot逻辑
=----
view 类似div,直接用
text 放文字
-----
wx:if="true"
wx:for="数据"
{{item}} {{index}} wx:key={{index}}控制台不报错
-----
{{item}} {{index}}
-----
简易留言板:
添加:
删除:
小作业:刷新留言还在
提醒:
wx.setStorage({
key:'name',
data:'value'
})
常见wxss
https://www.cnblogs.com/yang-shuai/p/6899430.html
https://gitee.com/httpchc320321/Notepad
22
splice() 方法向/从数组中添加/删除项目,然后返回被删除的项目。
arrayObject.splice(index,howmany,item1,.....,itemX)
splice(那开始,多少个,替换删除的)
index.wxml
{
添加
暂时无留言...
{{item.msg}}
}
index.wxss
{
/**index.wxss**/
.container{
/* margin: 30px; */
padding: 20px;
}
.view-box{
display: flex
}
input{
border: #ccc solid 2px;
padding: 5px;
border-radius: 20px
}
.place-input{
color: beige
}
.msg-info{
display: block;
margin: 10px 0 0 0;
color: #399;
}
.content{
margin: 20px 0 0 0;
}
.item{
overflow: hidden;
border-bottom: #ccc dashed 2px;
height:30px;
line-height: 30px;
}
.text{
float: left;
}
icon{
float: right;
margin: 5px 5px 0 0;
}
}
js
{
//index.js
//获取应用实例
const app = getApp()
Page({
data:{
msgData:[],
inputVal:'',
msgKeys:[]
},
onLoad(){
var vm=this;
wx.getStorageInfo({
success: function(res) {
console.log(res.keys);
vm.setData({
msgKeys:res.keys
})
var list=[]
for(var index in res.keys){
wx.getStorage({
key: res.keys[index],
success: function(res) {
console.log(res.data);
list.push({'msg':res.data})
vm.setData({
msgData:list
})
},
})
}
},
})
},
changeInputVal(e){
// console.log(e)
// e.detail.value输入的内容
this.setData({
inputVal:e.detail.value
})
},
del(ev){
// 索引
// console.log(ev.target.dataset.index);
var n=ev.target.dataset.index;
var list=this.data.msgData;
// 数组对象splice(那开始, 多少个, 替换删除的)
list.splice(n,1);
this.setData({
msgData:list
})
var keys=this.data.msgKeys[n];
wx.removeStorage({
key:keys,
success: function(res) {
console.log(res.data);
},
})
},
btn(){
// console.log(this.data.inputVal)
//error this.data.msgData.push({
// msg:this.data.inputVal
// });
//获取 再赋值list
var list=this.data.msgData;
list.push({
msg:this.data.inputVal
});
//更新
this.setData({
msgData:list,
inputVal:''
})
var oDate='s'+new Date().getTime();
wx.setStorage({
key: oDate,
data:this.data.inputVal,
})
}
})
}
data-index="{{index}}"
定义多少个 索引
微信官网文档
小程序的运行环境
运行环境 逻辑层 渲染层
iOS JavaScriptCore WKWebView
安卓 V8 chromium定制内核
小程序开发者工具 NWJS Chrome WebView
demo
https://github.com/wechat-miniprogram/miniprogram-demo
小程序注册 收费功能少..
https://cloud.tencent.com/developer/information/%E5%B0%8F%E7%A8%8B%E5%BA%8Fappid%E6%8E%A8%E8%8D%90%E5%85%8D%E8%B4%B9
https://cloud.tencent.com/developer/article/1361807
小程序的 AppID
小程序代码构成
.json 后缀的 JSON 配置文件
.wxml 后缀的 WXML 模板文件
.wxss 后缀的 WXSS 样式文件
.js 后缀的 JS 脚本逻辑文件
JSON 配置
JSON 是一种数据格式,并不是编程语言,在小程序中,JSON扮演的静态配置的角色
小程序配置 app.json
app.json 是当前小程序的全局配置,包括了小程序的所有页面路径、界面表现、网络超时时间、底部 tab 等。
pages小程序页面路径
window所有顶部属性color
工具配置 project.config.json 个性化配置,例如界面颜色、编译配置
页面配置 page.json
这里的 page.json 其实用来表示 pages/logs 目录下的 logs.json 这类和小程序页面相关的配置。
JSON 语法
数字,包含浮点数和整数
字符串,需要包裹在双引号中
Bool值,true 或者 false
数组,需要包裹在方括号中 []
对象,需要包裹在大括号中 {}
Null
还需要注意的是 JSON 文件中无法使用注释,试图添加注释将会引发报错。
WXML 模板
从事过网页编程的人知道,网页编程采用的是 HTML + CSS + JS 这样的组合,其中 HTML 是用来描述当前这个页面的结构,CSS 用来描述页面的样子,JS 通常是用来处理这个页面和用户的交互。
同样道理,在小程序中也有同样的角色,
其中 WXML 充当的就是类似 HTML 的角色。
打开 pages/index/index.wxml,你会看到以下的内容:
1.标签名字有点不一样
小程序的 WXML 用的标签是 view, button, text 等等
2.多了一些 wx:if 这样的属性以及 这样的表达式
提倡把渲染和逻辑分离。简单来说就是不要再让 JS 直接操控 DOM,
JS 只需要管理状态即可,然后再通过一种模板语法来描述状态和界面结构的关系即可。
WXML 是这么写 :
{{msg}}
JS 只需要管理状态即可:
this.setData({ msg: "Hello World" })
WXSS 样式
WXSS 具有 CSS 大部分的特性,小程序在 WXSS 也做了一些扩充和修改。
1.新增了尺寸单位。不同的宽度和设备像素比
rpx
2.提供了全局的样式和局部样式。
app.wxss全局 page.wxss当前页面
3.此外 WXSS 仅支持部分 CSS 选择器
MVVM 的开发模式(例如 React, Vue),
JS 逻辑交互 用户做交互
{{msg}}
click
点击 button 按钮的时候,我们希望把界面上 msg 显示成 "Hello World",于是我们在 button 上声明一个属性: bindtap ,在 JS 文件里边声明了 clickMe 方法来响应这次点击操作:
Page({
clickMe:function(){
this.setData({msg:"hello world"})
}
})
小程序宿主环境
微信客户端给小程序所提供的环境为宿主环境
渲染层和逻辑层
WXML 模板和 WXSS 样式工作在渲染层,
JS 脚本工作在逻辑层。
2个线程管理
渲染层的界面使用了多个页面多WebView 进行渲染
逻辑层采用JsCore线程运行JS脚本。
两个线程的通信会经由微信客户端Native
程序与页面
微信客户端在打开小程序之前,会把整个小程序的代码包下载到本地。
紧接着通过 app.json 的 pages 字段就可以知道你当前小程序的所有页面路径:
pages/index/index
这个小程序的首页(打开小程序看到的第一个页面)
小程序启动之后,在 app.js 定义的 App 实例的 onLaunch 回调会被执行:
App({
onLaunch: function () {
// 小程序启动之后 触发
}
})
整个小程序只有一个 App 实例,是全部页面共享的,
pages/logs/logs
微信客户端会先根据 logs.json 配置生成一个界面,顶部的颜色和文字你都可以在这个 json 文件里边定义好
logs.json-wxml-wxss-wxjs
logs.js
{
Page({
data: { // 参与页面渲染的数据
logs: []
},
onLoad: function () {
// 页面渲染后 执行
}
})
}
组件
小程序提供了丰富的基础组件给开发者,开发者可以像搭积木一样,组合各种组件拼合成自己的小程序。
HTML 的 div, p 等标签一样
WXML 写上对应的组件标签 组件显示在界面
界面上显示地图
longitude(中心经度) 和 latitude(中心纬度)
组件的内部行为也会通过事件的形式让开发者可以感知,例如用户点击了地图上的某个标记,你可以在 js 编写 markertap 函数来处理:
通过 style 或者 class 来控制组件的外层样式
API
为了让开发者可以很方便的调起微信提供的能力,例如获取用户信息、微信支付等等,小程序提供了很多 API 给开发者去使用。
获取用户的地理位置
wx.getLocation({
type: 'wgs84',
success: (res) => {
var latitude = res.latitude // 纬度
var longitude = res.longitude // 经度
}
})
调用微信扫一扫能力,只需要:
wx.scanCode({
success: (res) => {
console.log(res)
}
})
getLocation需要在app.json中声明permission字段
"permission":{
"scope.userLocation":{
"desc":"请点击确定"
}
},
https://blog.csdn.net/weixin_43953710/article/details/90769234
Longitude:113.27324 latitude:23.15792
https://developers.weixin.qq.com/miniprogram/dev/framework/quickstart/release.html
小程序协同工作和发布
项目管理
产品组 设计组 开发组 测试组
需求提出-设计-开发-体验-测试-发布
小程序成员管理
小程序成员管理包括对小程序项目成员及体验成员的管理。
项目成员:表示参与小程序开发、运营的成员,可登陆小程序管理后台,包括运营者、开发者及数据分析者。管理员可在“成员管理”中添加、删除项目成员,并设置项目成员的角色。
体验成员:表示参与小程序内测体验的成员,可使用体验版小程序,但不属于项目成员。管理员及项目成员均可添加、删除体验成员。
不同项目成员拥有不同的权限,从而保证小程序开发安全有序。
发布上线
一个小程序从开发完到上线一般要经过 预览-> 上传代码 -> 提交审核 -> 发布等步骤。
小程序开发指南
https://developers.weixin.qq.com/ebook?action=get_post_info&docid=0008aeea9a8978ab0086a685851c0a
第1章 小程序介绍与开发环境
1.1 Hello World
微信开发者工具下载
打开微信开发者工具,选择新建小程序项目,
我们先不需理解AppID的概念,
新建项目时选择无AppID,
并取消勾选“建立普通快速启动模板”的选项。
根目录下创建app.json
{
"pages":["pages/index/index"]
}
自动创建pages/index/ *
取消勾选“建立普通快速启动模板 new/old
https://jingyan.baidu.com/article/c85b7a64bf4d85003aac956e.html
https://www.jianshu.com/p/9ff0b424674a
pages/index/index.wxml
helloworld
pages/index/index.js
Pages({})
1.2.1 小程序技术发展历史
从技术的维度看,小程序并非凭空冒出来的一个概念。当微信中的 WebView 逐渐成为移动 Web 的一个重要入口时,微信就有相关的 JS API 了。
一些开发者应该对下面的代码有印象:
开放了拍摄、录音、语音识别、二维码、地图、支付、分享、卡券等几十个API
2015年初,微信发布了一整套网页开发工具包,称之为 JS-SDK,
“微信 Web 资源离线存储”。
直接从微信本地加载 Web 资源而不需要再从服务端拉取,
从而减少网页加载时间,为微信用户提供更优质的网页浏览体验。
每个公众号下所有 Web App 累计最多可缓存 5M 的资源。
运行环境
逻辑层
渲染层
iOS
JavaScriptCore
WKWebView
安卓
X5 JSCore
X5浏览器
小程序开发者工具
NWJS
Chrome WebView
1.4.1 申请AppID
点击 “开发”—“开发设置” 就可以看到小程序的 AppID
wx3ce92e92af9b29b1 my
第2章 小程序代码组成
小程序由配置代码JSON文件、模板代码 WXML 文件、样式代码 WXSS文件以及逻辑代码 JavaScript文件组成。
2.1 JSON 配置
JSON 是一种数据格式,并不是编程语言,在小程序中,JSON扮演的静态配置的角色。
app.json
{
"window":{
"navigationBarTitleText":"头标题",
名称"" 值 ,
}
}
json 不能注释
注意的是小程序是无法在运行过程中去动态更新JSON 配置文件从而发生对应的变化的。
语法
JSON文件都是被包裹在一个大括号中 {},通过key-value的方式来表达数据。
JSON的Key必须包裹在一个双引号中,在实践中,编写 JSON 的时候,忘了给 Key 值加双引号或者是把双引号写成单引号是常见错误。
JSON 文件中无法使用注释
{
JSON的值只能是以下几种数据格式:
数字,包含浮点数和整数
字符串,需要包裹在双引号中
Bool值,true 或者 false
数组,需要包裹在方括号中 []
对象,需要包裹在大括号中 {}
6.Null
}
2.2 WXML 模板
2.2 WXML 模板
WXML 全称是 WeiXin Markup Language,是小程序框架设计的一套标签语言,结合小程序的基础组件、事件系统,可以构建出页面的结构。
2.2.1 介绍
WXML 文件后缀名是 .wxml ,打开 pages/wxml/index.wxml 文件,有过 HTML 的开发经验的读者应该会很熟悉这种代码的书写方式,简单的 WXML语句在语法上同 HTML 非常相似。
pages/wxml/index.wxml
<标签名 属性名1="属性值1" 属性名2="属性值2" ...> ...
hello world
代码清单2-4 带有属性的图片标签
2.2.2 数据绑定
在 Web 开发中,开发者使用 JavaScript 通过Dom 接口来完成界面的实时更新
小程序中,使用 WXML 语言所提供的数据绑定功能,来完成此项功能。
数据绑定示例
当前时间:{{time}}
time 设置任何初始值,请打开 pages/wxml/index.js 文件
// pages/wxml/index.js
Page({
/**
* 页面的初始数据
*/
data: {
time: (new Date()).toString()
},
})
每次编译时间都会被更新
属性值也可以动态的去改变,有所不同的是,属性值必须被包裹在双引号中
hello world
变量名是大小写敏感的
没有被定义的变量的 wxml不显示
设置为 undefined 的变量 wxml显示
null 输出
2.2.3 逻辑语法
三元运算:
{{ a === 10? "变量 a 等于10": "变量 a 不等于10"}}
算数运算:
{{a + b}} + {{c}} + d
字符串的拼接
{{"hello " + name}}
直接放置数字、字符串或者是数组
{{[1,2,3]}}
{{"hello world"}}
2.2.4 条件逻辑
WXML 中,使用 wx:if="{{condition}}" 来判断是否需要渲染该代码块:
wx:if="{{condition}}"
True
使用 wx:elif 和 wx:else 来添加一个 else 块:
1
2
3
因为 wx:if 是一个控制属性,需要将它添加到一个标签上。如果要一次性判断多个组件标签,
可以使用一个 标签将多个组件包装起来,并在上边使用 wx:if 控制属性。
view1
view2
2.2.5 列表渲染
用 wx:for 控制属性绑定一个数组
默认数组的当前项的下标变量名默认为 index,数组当前项的变量名默认为 item
key=index value=item
{{index}}: {{item.message}}
使用 wx:for-item 指定数组当前元素的变量名,使用 wx:for-index 指定数组当前下标的变量名:
{{idx}}: {{itemName.message}}
类似 block wx:if ,也可以将 wx:for 用在 标签上,以渲染一个包含多节点的结构块。例如:
{{index}}:
{{item}}
如果列表中项目的位置会动态改变或者有新的项目添加到列表中,并且希望列表中的项目保持自己的特征和状态(如 中的输入内容, 的选中状态),需要使用 wx:key 来指定列表中项目的唯一的标识符。
wx:key 的值以两种形式提供:
1.字符串,代表在 for 循环的 array 中 item 的某个 property,该 property 的值需要是列表中唯一的字符串或数字,且不能动态改变。
2.保留关键字 this 代表在 for 循环中的 item 本身,这种表示需要 item 本身是一个唯一的字符串或者数字
当数据改变触发渲染层重新渲染的时候,会校正带有 key 的组件,框架会确保他们被重新排序,而不是重新创建,以确保使组件保持自身的状态,并且提高列表渲染时的效率。
bindtap="switch"
js
switch:
this.setData 设置data的数据
//[].length 不是从0开始 1开始
wxml
{
{{item.id}}
Switch
Add to the front
{{item}}
Add Number to the front
}
wxjs
{
Page({
data: {
objectArray: [
{id: 5, unique: 'unique_5'},
{id: 4, unique: 'unique_4'},
{id: 3, unique: 'unique_3'},
{id: 2, unique: 'unique_2'},
{id: 1, unique: 'unique_1'},
{id: 0, unique: 'unique_0'},
],
numberArray: [1, 2, 3, 4]
},
switch: function(e) {
const length = this.data.objectArray.length
for (let i = 0; i < length; ++i) {
const x = Math.floor(Math.random() * length)
const y = Math.floor(Math.random() * length)
const temp = this.data.objectArray[x]
this.data.objectArray[x] = this.data.objectArray[y]
this.data.objectArray[y] = temp
}
this.setData({
objectArray: this.data.objectArray
})
},
addToFront: function(e) {
const length = this.data.objectArray.length
this.data.objectArray = [{id: length, unique: 'unique_' + length}].concat(this.data.objectArray)
this.setData({
objectArray: this.data.objectArray
})
},
addNumberToFront: function(e){
this.data.numberArray = [ this.data.numberArray.length + 1 ].concat(this.data.numberArray)
this.setData({
numberArray: this.data.numberArray
})
}
})
}
圆圈滑动按钮
floor函数,其功能是“向下取整”,或者说“向下舍入”、“向零取舍”
注意
WXML 要求标签必须是严格闭合的
WXML中的属性是大小写敏感的
https://developers.weixin.qq.com/ebook?action=get_post_info&docid=000ee2c29d4f805b0086a37a254c0a
https://developers.weixin.qq.com/miniprogram/dev/framework/structure.html
2.2.6 模板
WXML提供模板(template),可以在模板中定义代码片段,然后在不同的地方调用。使用 name 属性,作为模板的名字。然后在 内定义代码片段
{
{{index}}: {{msg}}
Time: {{time}}
}
使用 is 属性,声明需要的使用的模板,然后将模板所需要的 data 传入,
{
{{index}}: {{msg}}
Time: {{time}}
}
对象扩展开
{{...item}}
is可以动态决定具体需要渲染哪个模板
{
odd
even
}
2.2.7 引用
WXML 提供两种文件引用方式import和include。
import include
import 可以在该文件中使用目标文件定义的 template
item.wxml
{{text}}
在 index.wxml 中引用了 item.wxml,就可以使用 item模板
import 有作用域的概念
import 不具有递归
import target的template
但不能 import target的template->import target的template
{
A
B
}
include 可以将目标文件中除了 外的整个代码引入,相当于是拷贝到 include 位置,
include 不能-template&css
{
代码清单2-22 index.wxml
body
代码清单2-23 header.wxml
header
代码清单2-24 footer.wxml
footer
}
2.2.8 共同属性
所有wxml 标签都支持的属性称之为共同属性
{
属性名 类型 描述 注解
id String 组件的唯一标识 整个页面唯一
class String 组件的样式类 在对应的 WXSS 中定义的样式类
style String 组件的内联样式 可以动态设置的内联样式
hidden Boolean 组件是否显示 所有组件默认显示
data-* Any 自定义属性 组件上触发的事件时,会发送给事件处理函数
bind*/catch* EventHandler 组件的事件
}
数据属性
绑定事件
2.3 WXSS 样式
WXSS(WeiXin Style Sheets)是一套用于小程序的样式语言,用于描述WXML的组件样式,也就是视觉上的效果。
WXSS与Web开发中的CSS类似。为了更适合小程序开发,WXSS对CSS做了一些补充以及修改。
2.3.1 文件组成
mod.wxss 其他
pages/index/index.wxss 页面样式
app.wxss 项目公共样式
页面样式:与app.json注册过的页面同名且位置同级的WXSS文件。
app.json注册了pages/rpx/index页面,那pages/rpx/index.wxss为页面pages/rpx/index.wxml的样式。
公共样式:根目录中的app.wxss为项目公共样式,它会被注入到小程序的每个页面。
2.3.2 尺寸单位
在WXSS中,引入了rpx(responsive pixel)尺寸单位。
rpx 适配不同宽度的屏幕
px为尺寸单位,有可能造成页面留白过多。
一个宽度为375物理像素的屏幕下,1rpx = 1px
2.3.3 WXSS引用
在CSS中,开发者可以这样引用另一个样式文件:@import url('./test_0.css')
@import url('./demo.css')
不会把demo.css合并到index.css中 会多一个.css的请求。
小程序中
@import './demo.wxss'
2.3.4 内联样式
WXSS内联样式与Web开发一致:
小程序支持动态更新内联样式
2.3.5 支持的选择器
{
类型 选择器 样例 样例描述
类选择器 .class .intro 选择所有拥有 class="intro" 的组件
id选择器 #id #firstname 选择拥有 id="firstname" 的组件
元素选择器 element view checkbox
选择所有文档的 view 组件和所有的 checkbox 组件
伪元素选择器 ::after view::after 在 view 组件后边插入内容
伪元素选择器 ::before view::before 在 view 组件前边插入内容
}
WXSS优先级与CSS类似, 权重
!important > style="" > #id> .class >element
权重越高越优先。在优先级相同的情况下,后设置的样式优先级高于先设置的样式。
2.3.6 官方样式库
WeUI.wxss基础样式库
WeUI是一套与微信原生视觉体验一致的基础样式库
由微信官方设计团队为微信内网页和微信小程序量身设计,令用户的使用感知更加统一。
包含button、cell、dialog、progress、toast、article、actionsheet、icon等各式原生。
具体使用文档可参考:https://github.com/Tencent/weui-wxss
用微信web开发者工具打开dist目录(请注意,是dist目录,不是整个项目)
组件的wxml结构请看dist/example/下的组件
样式文件可直接引用dist/style/weui.wxss,或者单独引用dist/style/widget下的组件的wxss
widget装置
2.4 JavaScript 脚本
小程序的主要开发语言是 JavaScript ,开发者使用 JavaScript 来开发业务逻辑以及调用小程序的 API 来完成业务需求。
2.4.1 ECMAScript
在大部分开发者看来,ECMAScript和JavaScript表达的是同一种含义,但是严格的说,两者的意义是不同的。ECMAScript是一种由Ecma国际通过ECMA-262标准化的脚本程序设计语言, JavaScript 是 ECMAScript 的一种实现。理解 JavaScript 是 ECMAScript 一种实现后,可以帮助开发者理解小程序中的 JavaScript同浏览器中的 JavaScript 以及 NodeJS 中的 JavaScript 是不相同的。
ECMA-262 规定了 ECMAScript 语言的几个重要组成部分:
语法
类型
语句
关键字
操作符
对象
浏览器中的JavaScript 是由 ECMAScript 和 BOM(浏览器对象模型)以及 DOM(文档对象模型)组成的,Web前端开发者会很熟悉这两个对象模型,它使得开发者可以去操作浏览器的一些表现,比如修改URL、修改页面呈现、记录数据等等。
NodeJS中的JavaScript 是由 ECMAScript 和 NPM以及Native模块组成,NodeJS的开发者会非常熟悉 NPM 的包管理系统,通过各种拓展包来快速的实现一些功能,同时通过使用一些原生的模块例如 FS、HTTP、OS等等来拥有一些语言本身所不具有的能力。
小程序中 JavaScript 构成
ECMAScript 小程序框架 小程序api
小程序中的 JavaScript 是由ECMAScript 以及小程序框架和小程序 API 来实现的。
比没有 BOM 以及 DOM 对象
JQuery、Zepto这种浏览器类库是无法在小程序中运行起来的
缺少 Native 模块和NPM包管理的机制,小程序中无法加载原生库,也无法直接使用大部分的 NPM 包。
2.4.2 小程序的执行环境
小程序的脚本执行环境也是有所区别
iOS平台,包括iOS9、iOS10、iOS11
Android平台
小程序IDE
iOS9和iOS10 所使用的运行环境并没有完全的兼容到 ECMAScript 6 标准
箭头函数
let const
模板字符串
…
小程序IDE提供语法转码工具
将 ECMAScript 6代码转为 ECMAScript 5代码
开发者需要在项目设置中,勾选 ES6 转 ES5 开启此功能。
>>>
详情 下开启
2.4.3 模块化
浏览器中,所有 JavaScript 是在运行在同一个作用域下的,定义的参数或者方法可以被后续加载的脚本访问或者改写。
同浏览器不同,
小程序中可以将任何一个JavaScript 文件作为一个模块,
通过module.exports 或者 exports 对外暴露接口。
module.exports exports
{
代码清单2-26 模块示例
// moduleA.js
module.exports = function( value ){
return value * 2;
}
代码清单2-27 引用模块A
// B.js
// 在B.js中引用模块A
var multiplyBy2 = require('./moduleA')
var result = multiplyBy2(4)
}
代码清单2-28 在需要使用这些模块的文件中,使用 require(path) 将公共代码引入
var common = require('common.js')
Page({
helloMINA: function() {
common.sayHello('MINA')
},
goodbyeMINA: function() {
common.sayGoodbye('MINA')
}
})
2.4.4 脚本的执行顺序
浏览器中,脚本严格按照加载的顺序执行
-----
{
代码清单2-29 浏览器中的脚本
以上代码的输出是:
a.js
inline script
b.js
}
小程序中的脚本执行顺序有所不同
小程序的执行的入口文件是 app.js
其中 require 的模块顺序决定文件的运行顺序
{
代码清单2-30 app.js
/* a.js
console.log('a.js')
*/
var a = require('./a.js')
console.log('app.js')
/* b.js
console.log('b.js')
*/
var b = require('./b.js')
以上代码的输出顺序是:
a.js
app.js
b.js
}
当 app.js 执行结束后,小程序会按照开发者在 app.json 中定义的 pages 的顺序,逐一执行
app.js->app.json-pages/js
2.4.5 作用域
同浏览器中运行的脚本文件有所不同,
小程序的脚本的作用域同 NodeJS 更为相似。
声明的变量和函数只在该文件中有效
不同的文件中可以声明相同名字的变量和函数
不会互相影响
当需要使用全局变量的时,通过使用全局函数 getApp() 获取全局的实例,
全局变量 全局函数getAPP()全局实例
{
代码清单2-38 在脚本 a.js 中设置全局变量
// a.js
// 获取全局变量
var global = getApp()
global.globalValue = 'globalValue'
代码清单2-39 在脚本 b.js 中访问 a.js 定义的全局变量
// b.js
// 访问全局变量
var global = getApp()
console.log(global.globalValue) // 输出 globalValue
}
只有在 a.js 比 b.js 先执行才有效
保证全局的数据可以在任何文件中安全的被使用到,那么可以在 App() 中进行设置
// app.js
App({
globalData: 1
})
代码清单2-41 获取以及修改 global 变量的方法
{
// a.js
// 局部变量
var localValue = 'a'
// 获取 global 变量
var app = getApp()
// 修改 global 变量
app.globalData++ // 执行后 globalData 数值为 2
}
i++ 赋值+
++i +赋值
获取 global 变量
{
// b.js
// 定义另外的局部变量,并不会影响 a.js 中文件变量
var localValue = 'b'
// 如果先执行了 a.js 这里的输出应该是 2
console.log(getApp().globalData)
}
https://developers.weixin.qq.com/ebook?action=get_post_info&docid=0004a2ef9b8f803b0086831c75140a
第3章 理解小程序宿主环境
3.1 渲染层和逻辑层
3.1.1 渲染“Hello World”页面
WXML模板使用 view 标签,其子节点用 {{ }} 的语法绑定一个 msg 的变量,
{{ msg }}
在 JS 脚本使用 this.setData 方法把 msg 字段设置成 “Hello World” ,
Page({
onLoad: function () {
this.setData({ msg: 'Hello World' })
}
})
1.渲染层和数据相关。 数据驱动
2.逻辑层负责产生、处理数据。
3.逻辑层通过 Page 实例的 setData 方法传递数据到渲染层。 通信模型
Page({}) 大写
3.1.2 通信模型
小程序的渲染层和逻辑层分别由2个线程管理
渲染层的界面使用了WebView 进行渲染
所以渲染层存在多个WebView线程
逻辑层采用JsCore线程运行JS脚本
js->微信客户端->wxml-wxss
3.1.3 数据驱动
数据驱动
状态和视图绑定在一起(状态变更时,视图也能自动变更)
视图html标签和变量状态
WXML结构实际上等价于一棵Dom树,通过一个JS对象也可以来表达Dom树的结构
a
view
view=a
js
{
name:"view"
children:[
{name:"view",children:[{text:"a"}]},
]
}
WXML结构转JS对象,再转Dom树
{{msg}} { name:"view", view
---> children:[{text:"hello"} ----> hello
{msg:'hell'} } ]
状态更新的时候,通过对比前后JS对象变化,进而改变视图层的Dom树
3.1.4 双线程下的界面渲染
逻辑层和渲染层是分开的两个线程
渲染层,宿主环境会把WXML转化成对应的JS对象
逻辑层发生数据变更 宿主环境提供的setData方法把数据从逻辑层传递到渲染层
逻辑层传递数据到渲染层
3.2 程序与页面
逻辑组成 一个小程序是由多个“页面”组成的“程序”
程序生命周期-逻辑处理
3.2.1 程序
“小程序”指的是产品层面的程序,而“程序”指的是代码层面的程序实例,为了避免误解,下文采用App来代替代码层面的“程序”概念。
概念读念
1. 程序构造器App()
宿主环境提供了 App() 构造器用来注册一个程序App,
App() 构造器 必须 项目根目录的app.js里
APP()->app.js
App实例是单例对象,
在其他JS脚本中可以使用宿主环境提供的 getApp() 来获取程序实例。
other.js->getApp()
// other.js
var appInstance = getApp()
App() 的调用方式
App构造器接受一个Object参数
其中onLaunch / onShow / onHide 三个回调是App实例的生命周期函数onError
App构造器
app.js
{
App({
onLaunch:function(option){}, 初始化一次
onShow:function(option){}, 后进前台,触发
onHide:function(){}, 前进后台,触发
onError:function(msg){}, jsErr,ApiErr时
globalData:'I am global data' fun|data AppInstance.this访问
})
}
表3-1 App构造器的参数
参数属性 类型 描述
onLaunch Function 当小程序初始化完成时,会触发 onLaunch(全局只触发一次)
onShow Function 当小程序启动,或从后台进入前台显示,会触发 onShow
onHide Function 当小程序从前台进入后台,会触发 onHide
onError Function 当小程序发生脚本错误,或者 API 调用失败时,会触发 onError 并带上错误信息
其他字段 任意 可以添加任意的函数或数据到 Object 参数中,在App实例回调用 this 可以访问
2. 程序的生命周期和打开场景
初始化:
进入小程序->微信初始化宿主环境->网络本地缓存小程序的代码包
->注入宿主环境->
App构造器参数所定义的onLaunch方法会被调用
微信->App实例->派发onLaunch事件
App构造器参数所定义的onHide方法会被调用。
用户点右上角的关闭|Home键->没直接销毁->小程序进入后台状态
App构造器参数所定义的onShow方法会被调用。
再次打开wx|xcx
小程序唤醒->小程序进入前台状态
App的生命周期
wx客户端 用户 主动触发 不主动调用 App实例的生命周期函数
打开小程序途径
群打 列表打 扫打 程序打程序
不同的业务处理
onLaunch和onShow的调用参数options
对不同打开方式调用不同参数
onLaunch和onShow带参数
App({
onLaunch: function(options) { console.log(options) },
onShow: function(options) { console.log(options) }
})
console:
{path: "pages/index/index", query: {…}, scene: 1001, shareTicket: undefined, referrerInfo: {…}, …}
onLaunch,onShow参数
{
字段 类型 描述
path String 打开小程序的页面路径
query Object 打开小程序的页面参数query
scene Number 打开小程序的场景值,详细场景值请参考小程序官方文档
shareTicket String shareTicket,详见小程序官方文档
referrerInfo Object 当场景为由从另一个小程序或公众号或App打开时,返回此字段
referrerInfo.appId String 来源小程序或公众号或App的 appId,详见下方说明
referrerInfo.extraData Object 来源小程序传过来的数据,scene=1037或1038时支持
}
referrerInfo.extraData
来源小程序传过来的数据 小程序打开小程序 从另一个小程序返回
referrerInfo.appId
场景支持返回
onLaunch: function(options)
options
{
场景值 场景 appId信息含义
1020 公众号 profile 页相关小程序列表 返回来源公众号 appId
1035 公众号自定义菜单 返回来源公众号 appId
1036 App 分享消息卡片 返回来源应用 appId
1037 小程序打开小程序 返回来源小程序 appId
1038 从另一个小程序返回 返回来源小程序 appId
1043 公众号模板消息 返回来源公众号 appId
}
3. 小程序全局数据
小程序的JS脚本是运行在JsCore的线程里
程序的每个页面各自有一个WebView线程进行渲染
切换页面 JS脚本同一个JsCore线程
不同页面直接可以通过App实例下的属性来共享数据
App构造器可以传递其他参数作为全局属性 全局共享数据的目的
{
// app.js
App({
globalData: 'I am global data' // 全局共享数据
})
// 其他页面脚本other.js
var appInstance = getApp()
console.log(appInstance.globalData) // 输出: I am global data
}
与此同时,我们要特别留意一点,所有页面的脚本逻辑都跑在同一个JsCore线程,页面使用setTimeout或者setInterval的定时器,然后跳转到其他页面时,这些定时器并没有被清除,需要开发者自己在页面离开的时候进行清理。
3.2.2 页面
一个小程序可以有很多页面,每个页面承载不同的功能,页面之间可以互相跳转。
1. 文件构成和路径
一个页面是分三部分组成
界面wxml&wxss、配置josn和逻辑js
注意:
一个页面的文件需要放置在同一个目录
WXML文件和JS文件必须存在 JSON和WXSS文件是可选的
默认pages字段的第一个页面路径为小程序的首页
页面路径-app.json-pages字段声明 注册到宿主环境
pages/index/page. 和 pages/other/other. (表示wxml/wxss/json/js四个文件)
去除.后缀
默认pages字段的第一个页面路径为小程序的首页
{
"pages":[
"pages/index/page", // 第一项默认为首页
"pages/other/other"
]
}
最后一个不加,err 不能注释
"pages/index/page"
路径/路径/文件统一.wxml/wxss/json/js 去后缀
2. 页面构造器Page()
宿主环境提供了 Page() 构造器用来注册一个小程序页面,
js-> Page()注册页面
Page构造器接受一个Object参数
data属性
当前页面WXML->数据绑定的初始数据
Page实例的生命周期函数
onLoad / onReady / onShow / onHide /onUnload 5个回调是Page实例的生命周期函数
页面的用户行为
onPullDownRefresh / onReachBottom / onShareAppMessage / onPageScroll 4个回调是页面的用户行为
Page构造器
Page({
data:{msg:'hello'}//{{msg}} //页面初始数据
页面生命周期5
onLoad:function(options){} 监听页面加载 1
onShow 页面显示 2
onReady:function(){} 页面初渲染 3
onHide 页面隐藏
onUnload 页面卸载
行为4
onPullDownRefresh:function(){} 下拉
onReachBottom: 上拉
onShareAppMessage 右上角转发
onPageScroll(){es6} 滚动
})
onLoad->onShow多次->onReady
onHide onunload 未知 应该是最后
Page构造器的参数
{
参数属性 类型 描述
data Object 页面的初始数据
onLoad Function 生命周期函数--监听页面加载,触发时机早于onShow和onReady
onReady Function 生命周期函数--监听页面初次渲染完成
onShow Function 生命周期函数--监听页面显示,触发事件早于onReady
onHide Function 生命周期函数--监听页面隐藏
onUnload Function 生命周期函数--监听页面卸载
onPullDownRefresh Function 页面相关事件处理函数--监听用户下拉动作
onReachBottom Function 页面上拉触底事件的处理函数
onShareAppMessage Function 用户点击右上角转发
onPageScroll Function 页面滚动触发事件的处理函数
其他 Any 可以添加任意的函数或数据,在Page实例的其他函数中用 this 可以访问
}
//Page实例的函数可以调用one 自定义的函数不能
one:'hello',
this.one
转发时
onHide
https://developers.weixin.qq.com/ebook?action=get_post_info&docid=0004eec99acc808b00861a5bd5280a
3. 页面的生命周期和打开参数
页面初次加载-wx给-Page实例-派发onLoad事件
Page构造器参数-onLoad方法会被调用
onLoad在页面-没被销毁之前只会触发1次
onLoad的回调中 打开参数option
页面显示之后
Page构造器参数-onShow调用
别页面-当前页面-都会调用
页面初次渲染完成时
onReady方法会被调用-onReady在页面没被销毁前只会触发1次,
页面已经准备妥当,逻辑层-视图层-交互
onLoad-onShow-onReady
页面不可见时
onHide调用
使用wx.navigateTo切换其他页面、底部tab切换时触发。
使用wx.redirectTo或wx.navigateBack返回到其他页时
页面会被微信客户端销毁回收
onUnload方法会被调用。
Page的生命周期-用户操作主动触发-不主动调用Page实例的生命周期函数
页面的打开参数query
在列表页打开-商品详情页时-商品的id传递过来
-的onLoad回调的参数option拿到Id-绘制出对应的商品
页面的打开参数Page构造器
// pages/list/list.js
// 列表页使用navigateTo跳转到详情页
wx.navigateTo({ url: 'pages/detail/detail?id=1&other=abc' })
// pages/detail/detail.js
Page({
onLoad: function(option) {
console.log(option.id)
console.log(option.other)
}
})
https://www.jb51.net/article/124573.htm
上面思路不行
列表一
Page({
onLoad: function(option) {
console.log(option.id)
console.log(option.other)
}
})
页面URL-网页的URL类似
分隔path和query部分
query部分的多个参数使用 & 进行分隔
key=value
onLoad的option可以拿到当前页面的打开参数,其类型是一个Object
特殊字符 采用UrlEncode后再拼接到页面URL
4. 页面的数据
界面渲染的基本原理
WXML-通过数据绑定-逻辑层传来-数据字段
数据
Page构造器的data字段
data参数
页面第一次-渲染时-从逻辑层-传递到-渲染层的数据。
{
{{text}}
{{array[0].msg}}
// page.js
Page({
data: {
text: 'init data',
array: [{msg: '1'}, {msg: '2'}]
}
})
}
宿主环境-Page实例-setData函数
Page实例下的方法
调用this.setData把数据传递给渲染层
同时可以更改
异步
setData的第二个参数是一个callback回调
this.setDate({},callback);
setData对界面渲染完毕后触发。
setData其一般调用格式是
setData(data, callback),
其中data是由多个key: value构成的Object对象。
setData({a:a,b:b})
{
// page.js
Page({
onLoad: function(){
this.setData({
text: 'change data'
}, function(){
// 在这次setData对界面渲染完毕后触发
})
}
})
}
改变的值进行设置 自动把新改动的字段 合并到渲染层对应的字段中
提高小程序的渲染性能
{
// page.js
Page({
data: {
a: 1, b: 2, c: 3,
d: [1, {text: 'Hello'}, 3, 4]
}
onLoad: function(){
// a需要变化时,只需要setData设置a字段即可
this.setData({a : 2})
}
})
}
注意3
1.直接修改 Page实例的this.data 而不调用 this.setData 是无法改变页面的状态的,还会造成数据不一致。
2.由于setData是需要两个线程的一些通信消耗,为了提高性能,每次设置的数据不应超过1024kB。
3.不要把data中的任意一项的value设为undefined,否则可能会有引起一些不可预料的bug。
直接修改this.data 调用
set this.setData
5. 页面的用户行为
宿主环境提供 四个和页面相关的用户行为回调
1.下拉刷新 onPullDownRefresh
app.json的window选项中
page.json中
置enablePullDownRefresh为true
wx.stopPullDownRefresh可以停止当前页面的下拉刷新。
2.上拉触底 onReachBottom
app.json的window选项中
page.json中
触发距离:onReachBottomDistance
在触发距离内滑动期间,本事件只会被触发一次。
上拉屏幕,滚动条触底时,触发onReachBottom事件
https://jingyan.baidu.com/article/bea41d43360d62f4c51be6ce.html
当数据的列表超出当前屏幕的高度 才能执行onReachBottom 加载
加多几个标签可以上啦下拉
上拉才到底触发
触底距离
json "onReachBottomDistance"=50
3.页面滚动 onPageScroll
滑动页面事件,参数为 Object,包含 scrollTop 字段,表示页面在垂直方向已滚动的距离(单位px)。
不用注册json内容 直接调用
同onReachBottom 加多内容 出现滚动 拉就触发onPageScroll
4.用户转发 onShareAppMessage
定义了此事件处理函数
右上角菜单才会显示“转发”按钮,
//自定此函数才出现转发...下 转|NO
转发按钮的时候会调用
需要return一个Object
包含title和path两个字段
{
// page.js
Page({
onShareAppMessage: function () {
return {
title: '自定义转发标题',
path: '/page/user?id=123'
}
}
})
}
6. 页面跳转和路由
一个小程序拥有多个页面,
我们可以通过wx.navigateTo推入一个新的页面,
如图3-6所示,在首页使用2次wx.navigateTo后,
页面层级会有三层,我们把这样的一个页面层级称为页面栈。
小程序宿主环境限制了这个页面栈的最大层级为10层
wx.navigateTo({ url: 'pageD' })
wx.navigateBack() 可以退出当前页面栈的最顶上页面
wx.redirectTo({ url: 'pageE' }) 是替换当前页变成pageE
页面栈-10层没法-增时,用redirectTo-页面跳转。
原生的Tabbar
app.json声明tabBar字段来定义Tabbar页
app.json定义小程序底部tab
{
{
"tabBar": {
"list": [
{ "text": "Tab1", "pagePath": "pageA" },
{ "text": "Tab1", "pagePath": "pageF" },
{ "text": "Tab1", "pagePath": "pageG" }
]
}
}
}
wx.switchTab({ url: 'pageF' })
原来的页面栈会被清空
页面栈变成 [ pageF ]
点击Tab1切回到pageA 不会再触发onLoad,没有被销毁。
wx.navigateTo和wx.redirectTo只能打开非TabBar页面
wx.switchTab只能打开Tabbar页面。
wx. reLaunch({ url: 'pageH' }) 重启小程序,
并且打开pageH -页面栈为
页面路由触发方式及页面生命周期函数的对应关系
{
路由方式 触发时机 路由前页面生命周期 路由后页面生命周期
初始化 小程序打开的第一个页面 onLoad, onShow
打开新页面 调用 API wx.navigateTo onHide onLoad, onShow
页面重定向 调用 API wx.redirectTo onUnload onLoad, onShow
页面返回 调用 API wx.navigateBack onUnload onShow
Tab 切换 调用 API wx.switchTab 请参考表3-6 请参考表3-6
重启动 调用 API wx.reLaunch onUnload onLoad, onShow
}
3.3 组件
页面多个部分组成,组件就是小程序页面的基本组成单元。
wx一系列基础组件
组件是在WXML声明中使用的,HTML语法相似,
标签名来引用一个组件,含开始标签和结束标签,
该标签的属性用来描述该组件。
所有组件名和属性都是小写,多个单词会以英文横杠 "-" 进行连接。
所有组件都拥有属性,
样式和事件绑定
{
属性名
类型
描述
其他说明
id
String
组件的唯一标示
保持整个页面唯一
class
String
组件的样式类
在对应的WXSS中定义的样式类
style
String
组件的内联样式
可以通过数据绑定进行动态设置的内联样式
hidden
Boolean
组件是否显示
所有组件默认显示
data-*
Any
自定义属性
组件上触发的事件时,会发送给事件处理函数
bind / catch
EventHandler
事件
详情见3.5节
}
组件都拥有各自自定义的属性,可以对该组件的功能或者样式进行修饰
Image图片组件属性
属性名 类型 默认值 描述
src String 图片资源地址
mode String 'scaleToFill' 图片裁剪、缩放的模式
lazy-load Boolean false 图片懒加载。只针对page与scroll-view下的image有效 1.5.0
binderror HandleEvent 当错误发生时触发事件,事件对象event.detail = {errMsg: 'something wrong'}
bindload HandleEvent 当图片载入完毕时触发事件,事件对象event.detail = {height:'图片高度px', width:'图片宽度px'}
bind不用加{{}}
data才加
binderror
https://www.jianshu.com/p/fb9165d56fd2
3.4 API
wx.API
wx.navigateTo可以保留当前页面 然后跳转到新的页面 10 多 wx.redirectTo
wx对象-宿主环境-提供的全局对象
所有小程序的API都挂载在wx对象底下(除了Page/App等特殊的构造器)
API分类
网络、媒体、文件、数据缓存、位置、设备、界面、界面节点信息还有一些特殊的开放接口
1.wx.on* API 监听事件.Callback函数作为参数。当该事件触发时,会调用 Callback 函数。
2.多数 API 接口为异步接口 ,都接受一个Object作为参数。
3.API的Object参数一般由success、fail、complete三个回调来接收接口调用结果
4.wx.get* 开头的API是获取宿主环境数据的接口。
5.wx.set* 开头的API是写入数据到宿主环境的接口。
通过wx.request发起网络请求
{
wx.request({
url: 'test.php',
data: {},
header: { 'content-type': 'application/json' },
success: function(res) {
// 收到https服务成功后返回
console.log(res.data)
},
fail: function() {
// 发生网络错误等情况触发
},
complete: function() {
// 成功或者失败后触发
}
})
}
不行失败 行 重新来学
API接口回调说明
参数名字 类型 必填 描述
success Function 否 接口调用成功的回调函数
fail Function 否 接口调用失败的回调函数
complete Function 否 接口调用结束的回调函数(调用成功、失败都会执行)
3.5 事件
3.5.1 什么是事件
UI界面用户互动,点击按钮,长按,这通知逻辑层,
将对应的处理状态呈现给用户。
不一定是用户主动触发的
视频video播放的过程
播放进度是会一直变化的
做相应的逻辑处理
这种“用户在渲染层的行为反馈”以及“组件的部分状态反馈”抽象为渲染层传递给逻辑层的“事件”
{
Click me!
// page.js
Page({
tapName: function(event) {
console.log(event)
}
})
}
bindtap这个属性绑定在组件
3.5.2 事件类型和事件对象
常见的事件类型
{
类型
触发条件
touchstart
手指触摸动作开始
touchmove
手指触摸后移动
touchcancel
手指触摸动作被打断,如来电提醒,弹窗
touchend
手指触摸动作结束
tap
手指触摸后马上离开
longpress
手指触摸后,超过350ms再离开,如果指定了事件回调函数并触发了这个事件,tap事件将不被触发
longtap
手指触摸后,超过350ms再离开(推荐使用longpress事件代替)
transitionend
会在 WXSS transition 或 wx.createAnimation 动画结束后触发
animationstart
会在一个 WXSS animation 动画开始时触发
animationiteration
会在一个 WXSS animation 一次迭代结束时触发
animationend
会在一个 WXSS animation 动画完成时触发
}
可以要组合bind catch 使用
bindtap触摸离开 catchtouchstart触摸开始
当事件回调触发的时候,会收到一个事件对象
事件对象属性
{
属性
类型
说明
type
String
事件类型
timeStamp
Integer
页面打开到触发事件所经过的毫秒数
target
Object
触发事件的组件的一些属性值集合
currentTarget
Object
当前组件的一些属性值集合
detail
Object
额外的信息
touches
Array
触摸事件,当前停留在屏幕中的触摸点信息的数组
changedTouches
Array
触摸事件,当前变化的触摸点信息的数组
}
currentTarget为当前事件所绑定的组件,而target则是触发该事件的源头组件。
{
点击我
// page.js
Page({
handleTap: function(evt) {
// 当点击inner节点时
// evt.target 是inner view组件
// evt.currentTarget 是绑定了handleTap的outer view组件
// evt.type == “tap”
// evt.timeStamp == 1542
// evt.detail == {x: 270, y: 63}
// evt.touches == [{identifier: 0, pageX: 270, pageY: 63, clientX: 270, clientY: 63}]
// evt.changedTouches == [{identifier: 0, pageX: 270, pageY: 63, clientX: 270, clientY: 63}]
}
})
}
catchtap绑定事件
target和currentTarget事件对象属性
{
属性
类型
说明
id
String
当前组件的id
tagName
String
当前组件的类型
dataset
Object
当前组件上由data-开头的自定义属性组成的集合
}
touch和changedTouches对象属性
{
属性
类型
说明
identifier
Number
触摸点的标识符
pageX, pageY
Number
距离文档左上角的距离,文档的左上角为原点 ,横向为X轴,纵向为Y轴
clientX, clientY
Number
距离页面可显示区域(屏幕除去导航条)左上角距离,横向为X轴,纵向为Y轴
}
3.5.3 事件绑定与冒泡捕获
事件绑定的写法和组件属性一致,以key="value"的形式,
valueDiv
1.key以bind或者catch开头,然后跟上事件的类型
bindtap、catchtouchstart
基础库版本1.5.0起,
bind和catch后可以紧跟一个冒号,其含义不变
bind:tap catch:touchstart
同时bind和catch前还可以加上capture-来表示捕获阶段。
capture-bind:捕获阶段 bind:默认冒泡
2.value是一个字符串
对应Page构造器-同名的函数
否则=控制台报错信息。
bind和capture-bind的含义分别代表事件的冒泡阶段和捕获阶段
bind下到上 capture-bind上到下
点击 inner view 会先后调用handleTap2、handleTap4、handleTap3、handleTap1。
{
outer view
inner view
}
bind事件绑定不会阻止冒泡事件向上冒泡,catch事件绑定可以阻止冒泡事件向上冒泡。
bind不禁止冒泡 catch禁止
capture-catch将中断捕获阶段和取消冒泡阶段
{
outer view
inner view
}
无特殊声明都是非冒泡事件
capture-catch
catch
在上述 WXML 中,如果按钮被点击,将触发 bindViewTap 和 bindButtonTap 两个事件,事件携带的 event.mark 将包含 myMark 和 anotherMark 两项。
Page({
bindViewTap: function(e) {
e.mark.myMark === "last" // true
e.mark.anotherMark === "leaf" // true
}
})
}
mark 和 dataset 很相似,主要区别在于: mark 会包含从触发事件的节点到根节点上所有的 mark: 属性值;而 dataset 仅包含一个节点的 data- 属性值。
细节注意事项:
如果存在同名的 mark ,父节点的 mark 会被子节点覆盖。
在自定义组件中接收事件时, mark 不包含自定义组件外的节点的 mark 。
不同于 dataset ,节点的 mark 不会做连字符和大小写转换
touches
touches 是一个数组,每个元素为一个 Touch 对象(canvas 触摸事件中携带的 touches 是 CanvasTouch 数组)。 表示当前停留在屏幕上的触摸点。
Touch 对象
{
identifier
Number
触摸点的标识符
pageX, pageY
Number
距离文档左上角的距离,文档的左上角为原点 ,横向为X轴,纵向为Y轴
clientX, clientY
Number
距离页面可显示区域(屏幕除去导航条)左上角距离,横向为X轴,纵向为Y轴
}
CanvasTouch 对象
{
identifier
Number
触摸点的标识符
x, y
Number
距离 Canvas 左上角的距离,Canvas 的左上角为原点 ,横向为X轴,纵向为Y轴
}
changedTouches
changedTouches 数据格式同 touches。 表示有变化的触摸点,如从无变有(touchstart),位置变化(touchmove),从有变无(touchend、touchcancel)
detail
自定义事件所携带的数据,如表单组件的提交事件会携带用户的输入,媒体的错误事件会携带错误信息,详见组件定义中各个事件的定义。
点击事件的detail 带有的 x, y 同 pageX, pageY 代表距离文档左上角的距离。
WXS响应事件
让事件在视图层(Webview)响应。小程序的框架分为视图层(Webview)和逻辑层(App Service),这样分层的目的是管控
WXS支持内置组件的事件,不支持自定义组件事件。
{
var wxsFunction = function(event, ownerInstance) {
var instance = ownerInstance.selectComponent('.classSelector') // 返回组件的实例
instance.setStyle({
"font-size": "14px" // 支持rpx
})
instance.getDataset()
instance.setClass(className)
// ...
return false // 不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault
}
}
ownerInstance
ComponentDescriptor
{
方法
参数
描述
selectComponent
selector对象
返回组件的 ComponentDescriptor 实例。
selectAllComponents
selector对象数组
返回组件的 ComponentDescriptor 实例数组。
setStyle
Object/string
设置组件样式,支持rpx。设置的样式优先级比组件 wxml 里面定义的样式高。不能设置最顶层页面的样式。
addClass/removeClass/ hasClass
string
设置组件的 class。设置的 class 优先级比组件 wxml 里面定义的 class 高。不能设置最顶层页面的 class。
getDataset
无
返回当前组件/页面的 dataset 对象
callMethod
(funcName:string, args:object)
调用当前组件/页面在逻辑层(App Service)定义的函数。funcName表示函数名称,args表示函数的参数。
requestAnimationFrame
Function
和原生 requestAnimationFrame 一样。用于设置动画。
getState
无
返回一个object对象,当有局部变量需要存储起来后续使用的时候用这个方法。
triggerEvent
(eventName, detail)
和组件的triggerEvent一致。
}
module.exports = {
touchmove: function(event, instance) {
console.log('log event', JSON.stringify(event))
},
propObserver: function(newValue, oldValue, ownerInstance, instance) {
console.log('prop observer', newValue, oldValue)
}
}
目前还不支持原生组件的事件、input和textarea组件的 bindinput 事件
支持交互动画
目前在WXS函数里面仅支持console.log方式打日志定位问题,注意连续的重复日志会被过滤掉
基础组件
框架为开发者提供了一系列基础组件,开发者可以通过组合这些基础组件进行快速开发
组件是视图层的基本组成单元。
组件自带一些功能与微信风格一致的样式。
一个组件通常包括 开始标签 和 结束标签,属性 用来修饰这个组件,内容 在两个标签之内。
所有组件与属性都是小写,以连字符-连接
属性类型
Boolean
Number
String
Array
Object
EventHandler 事件处理函数名
Any 任意属性
公共属性
id
class
style
hidden
data-*
bind*/catch*
特殊属性
几乎所有组件都有各自定义的属性,可以对该组件的功能或样式进行修饰
获取界面上的节点信息
WXML节点信息
{
const query = wx.createSelectorQuery()
query.select('#the-id').boundingClientRect(function(res){
res.top // #the-id 节点的上边界坐标(相对于显示区域)
})
query.selectViewport().scrollOffset(function(res){
res.scrollTop // 显示区域的竖直滚动位置
})
query.exec()
}
WXML节点布局相交状态
{
Page({
onLoad: function(){
wx.createIntersectionObserver().relativeToViewport().observe('.target-class', (res) => {
res.id // 目标节点 id
res.dataset // 目标节点 dataset
res.intersectionRatio // 相交区域占目标节点的布局区域的比例
res.intersectionRect // 相交区域
res.intersectionRect.left // 相交区域的左边界坐标
res.intersectionRect.top // 相交区域的上边界坐标
res.intersectionRect.width // 相交区域的宽度
res.intersectionRect.height // 相交区域的高度
})
}
})
}
{
Page({
onLoad: function(){
wx.createIntersectionObserver(this, {
thresholds: [0.2, 0.5]
}).relativeTo('.relative-class').relativeToViewport().observe('.target-class', (res) => {
res.intersectionRatio // 相交区域占目标节点的布局区域的比例
res.intersectionRect // 相交区域
res.intersectionRect.left // 相交区域的左边界坐标
res.intersectionRect.top // 相交区域的上边界坐标
res.intersectionRect.width // 相交区域的宽度
res.intersectionRect.height // 相交区域的高度
})
}
})
}
响应显示区域变化
显示区域尺寸
在手机上启用屏幕旋转支持
app.json 页面 json
{
"pageOrientation": "auto"
}
pageOrientation:landscape 固定为横屏
在 iPad 上启用屏幕旋转支持
app.json
"resizable": true
不能单独配置某个页面
Media Query
不同尺寸区域,布局差异。用 media query 解决。
{
.my-class {
width: 40px;
}
@media (min-width: 480px) {
/* 仅在 480px 或更宽的屏幕上生效的样式规则 */
.my-class {
width: 200px;
}
}
}
屏幕旋转事件
media query 无法控制 js 作为辅助
在 js 中读取页面的显示区域尺寸,可以使用 selectorQuery.selectViewport
{
Page({
onResize(res) {
res.size.windowWidth // 新的显示区域宽度
res.size.windowHeight // 新的显示区域高度
}
})
Component({
pageLifetimes: {
resize(res) {
res.size.windowWidth // 新的显示区域宽度
res.size.windowHeight // 新的显示区域高度
}
}
})
}
使用 wx.onWindowResize 来监听 不是推荐
动画
界面动画的常见方式
使用 CSS 渐变 和 CSS 动画 来创建简易的界面动画。
https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Transitions/Using_CSS_transitions
https://developer.mozilla.org/zh-CN/docs/Web/CSS/CSS_Animations/Using_CSS_animations
使用 wx.createAnimation 接口来动态创建简易的动画效果
动画过程中,可以使用 bindtransitionend bindanimationstart bindanimationiteration bindanimationend 来监听动画事件。
transitionend
CSS 渐变结束或 wx.createAnimation 结束一个阶段
animationstart
CSS 动画开始
animationiteration
CSS 动画结束一个阶段
animationend
CSS 动画结束
高级的动画方式
WXS 响应事件 的方式可以通过使用 WXS 来响应事件的方法来动态调整节点的 style 属性。通过不断改变 style 属性的值可以做到动画效果。同时,这种方式也可以根据用户的触摸事件来动态地生成动画。
连续使用 setData 来改变界面的方法也可以达到动画的效果。这样可以任意地改变界面,但通常会产生较大的延迟或卡顿,甚至导致小程序僵死。此时可以通过将页面的 setData 改为 自定义组件 中的 setData 来提升性能。下面的例子是使用 setData 来实现秒表动画的示例。
小程序的运行环境
:iOS(iPhone/iPad)、Android 和 用于调试的开发者工具
X5 基于 Mobile Chrome 57 内核
V8 中 XWeb 引擎基于 Mobile Chrome 67 内核
JavaScript 支持情况
运行限制
不支持动态执行 JS 代码
不支持使用 eval 执行 JS 代码
不支持使用 new Function 创建函数
支持了绝大部分的 ES6 API
小程序运行机制
前台/后台状态
冷启动:如果用户首次打开,或小程序销毁后被用户再次打开,此时小程序需要重新加载启动,即冷启动。
热启动:如果用户已经打开过某小程序,然后在一定时间内再次打开该小程序,此时小程序并未被销毁,只是从后台状态进入前台状态,这个过程就是热启动
只有当小程序进入后台一定时间,或者系统资源占用过高,才会被销毁
当小程序进入后台,可以会维持一小段时间的运行状态,如果这段时间内都未进入前台,小程序会被销毁。
当小程序占用系统资源过高,可能会被系统销毁或被微信客户端主动回收。
启动场景分类
A 类场景的重新启动策略
页面对应的 json
使得从某个页面退出后,下次 A 类场景的冷启动可以回到这个页面。
{
"restartStrategy": "homePage"
}
可选值 含义
homePage (默认值)如果从这个页面退出小程序,下次将从首页冷启动
homePageAndLatestPage 如果从这个页面退出小程序,下次冷启动后立刻加载这个页面,页面的参数保持不变(不可用于 tab 页)
退出状态
销毁之前,页面回调函数 onSaveExitState 会被调用
想保留页面中的状态,可以在这个回调函数中“保存”一些数据,下次启动时可以通过 exitState 获得这些已保存数据。
json
{
{
"restartStrategy": "homePageAndLatestPage"
}
}
js
{
Page({
onLoad: function() {
var prevExitState = this.exitState // 尝试获得上一次退出前 onSaveExitState 保存的数据
if (prevExitState !== undefined) { // 如果是根据 restartStrategy 配置进行的冷启动,就可以获取到
prevExitState.myDataField === 'myData'
}
},
onSaveExitState: function() {
var exitState = { myDataField: 'myData' } // 需要保存的数据
return {
data: exitState,
expireTimeStamp: Date.now() + 24 * 60 * 60 * 1000 // 超时时刻
}
}
})
}
onSaveExitState 返回值可以包含两项:
字段名 类型 含义
data Any 需要保存的数据(只能是 JSON 兼容的数据)
expireTimeStamp Number 超时时刻,在这个时刻后,保存的数据保证一定被丢弃,默认为 (当前时刻 + 1 天)
小程序更新机制
未启动时更新
用户下次打开时会先更新最新版本再打开。
启动时更新
冷启动时,都会检查是否有更新版本
新版本的小程序需要等下一次冷启动才会应用
马上应用最新版本
用 wx.getUpdateManager API 进行处理
{
const updateManager = wx.getUpdateManager()
updateManager.onCheckForUpdate(function (res) {
// 请求完新版本信息的回调
console.log(res.hasUpdate)
})
updateManager.onUpdateReady(function () {
wx.showModal({
title: '更新提示',
content: '新版本已经准备好,是否重启应用?',
success(res) {
if (res.confirm) {
// 新的版本已经下载好,调用 applyUpdate 应用新版本并重启
updateManager.applyUpdate()
}
}
})
})
updateManager.onUpdateFailed(function () {
// 新版本下载失败
})
}
半->时->1->个
自定义组件
创建自定义组件
类似于页面,
一个自定义组件由 json wxml wxss js 4个文件组成
1.首先在 json 自定义组件 声明
{
"component": true
}
2.在 wxml 文件中编写组件模板,在 wxss 文件中加入组件样式,它们的写法与页面的写法类似
wxml
{
{{innerText}}
}
wxss
{
/* 这里的样式只应用于这个自定义组件 */
.inner {
color: red;
}
}
3.在组件wxss中不应使用 ID选择器、属性选择器 和 标签名选择器。
4.自定义组件的 js, Component() 来注册组件
并提供 组件的属性定义、内部数据和 自定义方法。
properties属性 data数据 diy()自定义方法类似正常
{
Component({
properties: {
// 这里定义了innerText属性,属性值可以在组件使用时指定
innerText: {
type: String,
value: 'default value',
}
},
data: {
// 这里是一些组件内部数据
someData: {}
},
methods: {
// 这里是一个自定义方法
customMethod: function(){}
}
})
使用自
}
5.使用自定义组件引用声明
页面的 json 引用声明
提供每个自定义组件的 标签名 和 对应的自定义组件 文件路径
{
{
"usingComponents": {
"component-tag-name": "path/to/the/custom/component"
}
}
}
6.页面使用自定义组件
json声明的标签名
inner-text==4. properties属性 innerText
注意
WXML 节点标签名 只能小写字母、中划线和下划线的组合
自定义组件也是可以引用自定义组件的 json usingComponents关键字
自定义组件和页面所在项目根目录名不能以“wx-”为前缀
usingComponents 会使得页面的 this 对象的原型稍有差异
Object.getPrototypeOf(this) 结果不同。
多一些方法,如 selectComponent
setData 内容不会被直接深复制
页面比较复杂,新增或删除 usingComponents 定义段时建议重新测试一下
组件模板和样式
类似于页面,自定义组件拥有自己的 wxml 模板和 wxss 样式
组件模板
组件模板的写法与页面模板相同
组件模板与组件数据结合后生成的节点树,将被插入到组件的引用位置上。
在组件模板中可以提供一个 节点,用于承载组件引用时提供的子节点。
这里是组件的内部节点
这里是插入到组件slot中的内容
组件模板=引用组件component-tag-name->view
模板数据绑定
与普通的 WXML 模板类似,可以使用数据绑定,这样就可以向子组件的属性传递动态数据。
组件wxml的slot
默认情况下,一个组件的wxml中只能有一个slot。需要使用多slot时,可以在组件js中声明启用。
Component({
options: {
multipleSlots: true // 在组件定义时的选项中启用多slot支持
},
properties: { /* ... */ },
methods: { /* ... */ }
})
组件的wxml中使用多个slot,以不同的 name 来区分
这里是组件的内部细节
使用时
这里是插入到组件slot name="before"中的内容
这里是插入到组件slot name="after"中的内容
组件样式
组件对应 wxss 文件的样式,只对组件wxml内的节点生效。
组件和引用组件的页面不能使用id选择器(#a)、属性选择器([a])和标签名选择器,
请改用class选择器。
组件和引用组件的页面中使用后代选择器(.a .b)在一些极端情况下会有非预期的表现,如遇,请避免使用。
子元素选择器(.a>.b)只能用于 view 组件与其子节点之间,用于其他组件可能导致非预期的情况。
继承样式,如 font 、 color ,会从组件外继承到组件内。
除继承样式外, app.wxss 中的样式、组件所在页面的的样式对自定义组件无效(除非更改组件样式隔离选项)。
#a { } /* 在组件中不能使用 */
[a] { } /* 在组件中不能使用 */
button { } /* 在组件中不能使用 */
.a > .b { } /* 除非 .a 是 view 组件节点,否则不一定会生效 */
组件可以指定它所在节点的默认样式,使用 :host 选择器
/* 组件 custom-component.wxss */
:host {
color: yellow;
}
这段文本是黄色的
组件样式隔离
自定义组件的样式只受到自定义组件 wxss 的影响
除非以下两种情况:
app.wxss 或页面的 wxss 中使用了标签名选择器(或一些其他特殊选择器)来直接指定样式,这些选择器会影响到页面和全部组件。通常情况下这是不推荐的做法。
指定特殊的样式隔离选项 styleIsolation 。
Component({
options: {
styleIsolation: 'isolated'
}
})
isolated 表示启用样式隔离,在自定义组件内外,使用 class 指定的样式将不会相互影响(一般情况下的默认值);
apply-shared 表示页面 wxss 样式将影响到自定义组件,但自定义组件 wxss 中指定的样式不会影响页面;
shared 表示页面 wxss 样式将影响到自定义组件,自定义组件 wxss 中指定的样式也会影响页面和其他设置了 apply-shared 或 shared 的自定义组件。(这个选项在插件中不可用。)
额外的样式
page-isolated 表示在这个页面禁用 app.wxss ,同时,页面的 wxss 不会影响到其他自定义组件;
page-apply-shared 表示在这个页面禁用 app.wxss ,同时,页面 wxss 样式不会影响到其他自定义组件,但设为 shared 的自定义组件会影响到页面;
page-shared 表示在这个页面禁用 app.wxss ,同时,页面 wxss 样式会影响到其他设为 apply-shared 或 shared 的自定义组件,也会受到设为 shared 的自定义组件的影响。
Component 的 options 中设置 addGlobalClass: true 。
这个选项等价于设置 styleIsolation: apply-shared ,
但设置了 styleIsolation 选项后这个选项会失效。
/* 组件 custom-component.js */
Component({
options: {
addGlobalClass: true,
}
})
这段文本的颜色由 `app.wxss` 和页面 `wxss` 中的样式定义来决定
/* app.wxss */
.red-text {
color: red;
}
外部样式类
组件接受外部传入的样式类。
此时可以在
Component 中用 externalClasses
定义段定义若干个外部样式类。
/* 组件 custom-component.js */
Component({
externalClasses: ['my-class']
})
这段文本的颜色由组件外的 class 决定
引用页面或父组件的样式
即使启用了样式隔离 isolated ,组件仍然可以在局部引用组件所在页面的样式或父组件的样式。
页面 wxss
.blue-text {
color: blue;
}
这个组件中可以使用
这段文本是蓝色的
父组件 wxss
.red-text {
color: red;
}
这个组件
这段文本是红色的
也可以连续使用多个 ^ 来引用祖先组件中的样式。
注意:如果组件是比较独立、通用的组件,请优先使用外部样式类的方式,而非直接引用父组件或页面的样式。
Component 构造器
Component 构造器可用于定义组件,调用 Component 构造器时可以指定组件的属性、数据、方法等。
{
Component({
behaviors: [],
properties: {
myProperty: { // 属性名
type: String,
value: ''
},
myProperty2: String // 简化的定义方式
},
data: {}, // 私有数据,可用于模板渲染
lifetimes: {
// 生命周期函数,可以为函数,或一个在methods段中定义的方法名
attached: function () { },
moved: function () { },
detached: function () { },
},
// 生命周期函数,可以为函数,或一个在methods段中定义的方法名
attached: function () { }, // 此处attached的声明会被lifetimes字段中的声明覆盖
ready: function() { },
pageLifetimes: {
// 组件所在页面的生命周期函数
show: function () { },
hide: function () { },
resize: function () { },
},
methods: {
onMyButtonTap: function(){
this.setData({
// 更新属性和数据的方法与更新页面数据的方法类似
})
},
// 内部方法建议以下划线开头
_myPrivateMethod: function(){
// 这里将 data.A[0].B 设为 'myPrivateData'
this.setData({
'A[0].B': 'myPrivateData'
})
},
_propertyChange: function(newVal, oldVal) {
}
}
})
}
使用 Component 构造器构造页面
小程序的页面也可以视为自定义组件。因而,页面也可以使用 Component 构造器构造
{
"usingComponents": {}
}
{
Component({
properties: {
paramA: Number,
paramB: String,
},
methods: {
onLoad: function() {
this.data.paramA // 页面参数 paramA 的值
this.data.paramB // 页面参数 paramB 的值
}
}
})
}
Component 构造器来构造页面的一个好处是可以使用 behaviors 来提取所有页面中公用的代码段。
{
// page-common-behavior.js
module.exports = Behavior({
attached: function() {
// 页面创建时执行
console.info('Page loaded!')
},
detached: function() {
// 页面销毁时执行
console.info('Page unloaded!')
}
})
// 页面 A
var pageCommonBehavior = require('./page-common-behavior')
Component({
behaviors: [pageCommonBehavior],
data: { /* ... */ },
methods: { /* ... */ },
})
// 页面 B
var pageCommonBehavior = require('./page-common-behavior')
Component({
behaviors: [pageCommonBehavior],
data: { /* ... */ },
methods: { /* ... */ },
})
}
可以用 es6 来代替 extents
组件间通信与事件
组件间通信
WXML 数据绑定:用于父组件向子组件的指定属性设置数据,仅能设置 JSON 兼容数据(
parent->child json
事件:用于子组件向父组件传递数据,可以传递任意数据
child->parent 任意
父组件还可以通过 this.selectComponent 方法获取子组件实例对象,这样就可以直接访问组件的任意数据和方法
监听事件
监听自定义组件事件的方法与监听基础组件事件的方法完全一致
{
Page({
onMyEvent: function(e){
e.detail // 自定义组件触发事件时提供的detail对象
}
})
}
触发事件
自定义组件触发事件时,需要使用 triggerEvent 方法,指定事件名、detail对象和事件选项:
{
点击这个按钮将触发“myevent”事件
Component({
properties: {},
methods: {
onTap: function(){
var myEventDetail = {} // detail对象,提供给事件监听函数
var myEventOption = {} // 触发事件的选项
this.triggerEvent('myevent', myEventDetail, myEventOption)
}
}
})
}
选项名 类型 是否必填 默认值 描述
bubbles Boolean 否 false 事件是否冒泡
composed Boolean 否 false 事件是否可以穿越组件边界,为false时,事件将只能在引用组件的节点树上触发,不进入其他任何组件内部
capturePhase Boolean 否 false 事件是否拥有捕获阶段
{
// 页面 page.wxml
// 组件 another-component.wxml
// 组件 my-component.wxml
// 组件 my-component.js
Component({
methods: {
onTap: function(){
this.triggerEvent('customevent', {}) // 只会触发 pageEventListener2
this.triggerEvent('customevent', {}, { bubbles: true }) // 会依次触发 pageEventListener2 、 pageEventListener1
this.triggerEvent('customevent', {}, { bubbles: true, composed: true }) // 会依次触发 pageEventListener2 、 anotherEventListener 、 pageEventListener1
}
}
})
}
qqbrower 忽略空格
注意 到时 用多少 找多少 看 过
组件生命周期
最重要的生命周期是 created attached detached ,包含一个组件实例生命流程的最主要时间点。
组件实例刚刚被创建好时, created 生命周期被触发
在组件完全初始化完毕、进入页面节点树后, attached 生命周期被触发
this.data才可以使用
在组件离开页面节点树后, detached 生命周期被触发
定义生命周期方法
{
Component({
lifetimes: {
attached: function() {
// 在组件实例进入页面节点树时执行
},
detached: function() {
// 在组件实例被从页面节点树移除时执行
},
},
// 以下是旧式的定义方式,可以保持对 <2.2.3 版本基础库的兼容
attached: function() {
// 在组件实例进入页面节点树时执行
},
detached: function() {
// 在组件实例被从页面节点树移除时执行
},
// ...
})
}
behaviors 中也可以编写生命周期方法
生命周期 参数 描述 最低版本
created 无 在组件实例刚刚被创建时执行 1.6.3
attached 无 在组件实例进入页面节点树时执行 1.6.3
ready 无 在组件在视图层布局完成后执行 1.6.3
moved 无 在组件实例被移动到节点树另一个位置时执行 1.6.3
detached 无 在组件实例被从页面节点树移除时执行 1.6.3
error Object Error 每当组件方法抛出错误时执行 2.4.1
组件所在页面的生命周期
pageLifetimes 定义段中定义
生命周期 参数 描述 最低版本
show 无 组件所在的页面被展示时执行 2.2.3
hide 无 组件所在的页面被隐藏时执行 2.2.3
resize Object Size 组件所在的页面尺寸变化时执行 2.4.0
{
Component({
pageLifetimes: {
show: function() {
// 页面被展示
},
hide: function() {
// 页面被隐藏
},
resize: function(size) {
// 页面尺寸变化
}
}
})
}
behaviors
行为 | 授权
每个 behavior 可以包含一组属性、数据、生命周期函数和方法。
组件引用它时,它的属性、数据和方法会被合并到组件中,生命周期函数也会在对应时机被调用。
{
// my-component.js
var myBehavior = require('my-behavior')
Component({
behaviors: [myBehavior],
properties: {
myProperty: {
type: String
}
},
data: {
myData: {}
},
attached: function(){},
methods: {
myMethod: function(){}
}
})
}
字段的覆盖和组合规则
内置 behaviors
自定义组件可以通过引用内置的 behavior 来获得内置组件的一些行为。
Component({
behaviors: ['wx://form-field']
})
{
// 自定义组件 my-component 内部
Component({
behaviors: ['wx://component-export'],
export() {
return { myField: 'myValue' }
}
})
this.selectComponent('#the-id') // 等于 { myField: 'myValue' }
}
组件间关系
定义和使用组件间关系
关联一类组件
relations 定义段
数据监听器
数据监听器可以用于监听和响应任何属性和数据字段的变化。
使用数据监听器
{
Component({
attached: function() {
this.setData({
numberA: 1,
numberB: 2,
})
},
observers: {
'numberA, numberB': function(numberA, numberB) {
// 在 numberA 或者 numberB 被设置时,执行这个函数
this.setData({
sum: numberA + numberB
})
}
}
})
}
监听字段语法
据监听器支持监听属性或内部数据的变化,可以同时监听多个。一次 setData 最多触发每个监听器一次。
仅使用通配符 ** 可以监听全部 setData
Component({
observers: {
'**': function() {
// 每次 setData 都触发
},
},
})
纯数据字段
纯数据字段是一些不用于界面渲染的 data 字段,可以用于提升页面更新性能。
{
Component({
options: {
pureDataPattern: /^_/ // 指定所有 _ 开头的数据字段为纯数据字段
},
data: {
a: true, // 普通数据字段
_b: true, // 纯数据字段
},
methods: {
myMethod() {
this.data._b // 纯数据字段可以在 this.data 中获取
this.setData({
c: true, // 普通数据字段
_d: true, // 纯数据字段
})
}
}
})
}
这行会被展示
这行不会被展示
抽象节点
自定义组件扩展
为了更好定制自定义组件的功能,可以使用自定义组件扩展机制
码农 搬砖 多搬学练 旧了 自然会了 重构
开发第三方自定义组件
命令行工具
官方提供了命令行工具,用于快速初始化一个项目。
https://github.com/wechat-miniprogram/miniprogram-cli
npm install -g @wechat-miniprogram/miniprogram-cli
新建一个空目录作为项目根目录
miniprogram init --type custom-component
第一次使用 miniprogram init 初始化项目会去 github 上拉取模板,因此需要保证网络畅通
官方提供的自定义组件
weui-miniprogram recycle-view
自定义组件扩展示例
computed
单元测试
自定义组件测试工具集
npm i --save-dev miniprogram-simulate
编写测试用例
插件
开发插件
由于插件需要 appid 才能工作,请填入一个 appid;
手动将 appid 填写到 miniprogram/app.json
插件目录结构
插件执行页面跳转的时候,可以使用 navigator 组件。当插件跳转到自身页面时, url 应设置为这样的形式:plugin-private://PLUGIN_APPID/PATH/TO/PAGE 。需
快 这速度 不打 没看完 新出又旧
使用插件
标题 过 用c来
插件调用 API 的限制
插件可以调用的 API 与小程序不同,主要有两个区别:
插件的请求域名列表与小程序相互独立;
一些 API 不允许插件调用(这些函数不存在于 wx 对象下)
wx.arrayBufferToBase64
wx.base64ToArrayBuffer
发起请求
wx.request
上传、下载
API 最低版本 备注
wx.downloadFile 1.9.6
wx.uploadFile 1.9.6
WebSocket
API 最低版本 备注
wx.connectSocket 1.9.6
图片
录音
实时音视频
录音管理
音频播放控制
音乐播放控制
背景音频播放管理
音频组件控制
视频
数据缓存 同样xcx
获取位置
网络状态
扫码
....
插件使用组件的限制
开放能力(open-type)为以下之一的 button:
contact(打开客服会话)
getPhoneNumber(获取用户手机号)
getUserInfo(获取用户信息)
open-data
web-view
navigator 需要基础库版本 2.1.0
live-player 和 live-pusher 需要基础库版本 2.3.0
插件功能页
某些接口不能在插件中直接调用(如 wx.login)
功能页不能使用 wx.navigateTo 来进行跳转,而是需要一个名为 functional-page-navigator 的组件。
功能页的跳转目前不支持在开发者工具中调试,请在真机上测试
用户信息功能页
调用参数
用户信息功能页使用 functional-page-navigator 进行跳转时,对应的参数 name 应为固定值 loginAndGetUserInfo,其余参数与 wx.getUserInfo 相同,
过 到时用再
doc不能教程
支付功能页
插件使用支付功能,需要进行额外的权限申请,申请位置位于管理后台的“小程序插件 -> 基本设置 -> 支付能力”设置项中。
主体为个人小程序在使用插件时,都无法正常使用插件里的支付功能
functional-page-navigator 进行跳转时
wx.request
请求 wx 支付api
收货地址功能页
收货地址功能页用于展示用户的收货地址列表,用户可以选择其中的收货地址。
调用参数
用户信息功能页使用 functional-page-navigator 进行跳转时,
对应的参数 name 应为固定值
chooseAddress ,返回参数与 wx.chooseAddress 相同。
chooseAddress
选择收货地址
wx多会旧-qtg多 过 看 下 就好 到时
小程序 官网 第三方 插件 类库
dsg
网络
服务器域名配置
普通 HTTPS 请求(wx.request)、
上传文件(wx.uploadFile)、
下载文件(wx.downloadFile) 和
WebSocket 通信(wx.connectSocket)
网络接口允许与局域网 IP 通信,但要注意 不允许与本机 IP 通信
UDP 通信(wx.createUDPSocket)。
跳过域名校验
在微信开发者工具中,可以临时开启 开发环境不校验请求域名、TLS版本及HTTPS证书 选项,跳过服务器域名的校验。此时,在微信开发者工具中及手机开启调试模式时,不会进行服务器域名的校验。
在服务器域名配置成功后,建议开发者关闭此选项进行开发,并在各平台下进行测试,以确认服务器域名配置正确。
局域网通信
wx.startLocalServiceDiscovery 等一系列 mDNS API,可以用来获取局域网内提供 mDNS 服务的设备的 IP。
wx.request/wx.connectSocket/wx.uploadFile/wx.downloadFile
的 url 参数允许为 ${IP}:${PORT}/${PATH} 的格式,
当且仅当 IP 与手机 IP 处在同一网段且不与本机 IP 相同
不会进行安全域的校验,不要求必须使用 https/wss,也可以使用 http/ws。
wx.request({
url: 'http://10.9.176.40:828'
// 省略其他参数
})
wx.connectSocket({
url: 'ws://10.9.176.42:828'
// 省略其他参数
})
mDNS
目前小程序只支持通过 mDNS 协议获取局域网内其他设备的 IP。
iOS 上 mDNS API 的实现基于 Bonjour,Android 上则是基于 Android 系统接口。
存储
每个微信小程序都可以有自己的本地缓存,可以通过
wx.setStorage/wx.setStorageSync、
wx.getStorage/wx.getStorageSync、
wx.clearStorage/wx.clearStorageSync,
wx.removeStorage/wx.removeStorageSync
对本地缓存进行读写和清理
隔离策略
同一个微信用户,同一个小程序 storage 上限为 10MB。storage 以用户维度隔离,
同一台设备上,A 用户无法读取到 B 用户的数据;不同小程序之间也无法互相读写数据。
清理策略
本地缓存的清理时机跟代码包一样,只有在代码包被清理的时候本地缓存才会被清理。
文件系统
文件系统是小程序提供的一套以小程序和用户维度隔离的存储以及一套相应的管理接口。
。通过 wx.getFileSystemManager()
可以获取到全局唯一的文件系统管理器,
所有文件系统的管理操作通过 FileSystemManager 来调用。
var fs = wx.getFileSystemManager()
文件主要分为两大类:
代码包文件:代码包文件指的是在项目目录中添加的文件。
本地文件:通过调用接口本地产生,或通过网络下载下来,存储到本地的文件。
本地文件又分为三种:
本地临时文件:临时产生,随时会被回收的文件。不限制存储大小。
本地缓存文件:小程序通过接口把本地临时文件缓存后产生的文件,不能自定义目录和文件名。跟本地用户文件共计,普通小程序最多可存储 10MB,游戏类目的小程序最多可存储 50MB。
本地用户文件:小程序通过接口把本地临时文件缓存后产生的文件,允许自定义目录和文件名。跟本地缓存文件共计,普通小程序最多可存储 10MB,游戏类目的小程序最多可存储 50MB。
代码包文件的访问方式是从项目根目录开始写文件路径,不支持相对路径的写法。
如:/a/b/c、a/b/c 都是合法的,./a/b/c ../a/b/c 则不合法。
{{协议名}}://文件路径
协议名在 iOS/Android 客户端为 "wxfile",在开发者工具上为 "http"
不可把本地临时文件路径存储起来下次使用。
如果需要下次在使用,可通过 FileSystemManager.saveFile() 或 FileSystemManager.copyFile() 接口把本地临时文件转换成本地缓存文件或本地用户文件。
示例
本地缓存文件
本地缓存文件只能通过调用特定接口产生,不能直接写入内容。本地缓存文件产生后,重启之后仍可用。本地缓存文件只能通过 FileSystemManager.saveFile() 接口将本地临时文件保存获得。
示例
本地用户文件
提供了一个用户文件目录给开发者,开发者对这个目录有完全自由的读写权限。通过 wx.env.USER_DATA_PATH 可以获取到这个目录的路径
// 在本地用户文件目录下创建一个文件 hello.txt,写入内容 "hello, world"
const fs = wx.getFileSystemManager()
fs.writeFileSync(`${wx.env.USER_DATA_PATH}/hello.txt`, 'hello, world', 'utf8')
清理策略
本地临时文件只保证在小程序当前生命周期内,一旦小程序被关闭就可能被清理,即下次冷启动不保证可用。
本地缓存文件和本地用户文件的清理时机跟代码包一样,只有在代码包被清理的时会被清理。
Canvas 画布
所有在 canvas 中的画图必须用 JavaScript 完成
onLoad 中
第一步:创建一个 Canvas 绘图上下文
const ctx = wx.createCanvasContext('myCanvas')
第二步:使用 Canvas 绘图上下文进行绘图描述
ctx.setFillStyle('red')
ctx.fillRect(10, 10, 150, 75)
第三步:画图
ctx.draw()
我们可以在 canvas 中加上一些事件,来观测它的坐标系
Coordinates: ({{x}}, {{y}})
x: e.touches[0].x,
y: e.touches[0].y
start (e) {
this.setData({
hidden: false,
x: e.touches[0].x,
y: e.touches[0].y
})
},
渐变
createLinearGradient(x, y, x1, y1) 创建一个线性的渐变
createCircularGradient(x, y, r) 创建一个从圆心开始的渐变
addColorStop(position, color) 方法用于指定颜色渐变点的位置和颜色,位置必须位于0到1之间。
同css3...
setFillStyle 和 setStrokeStyle 方法设置渐变,
{
const ctx = wx.createCanvasContext('myCanvas')
// Create linear gradient
const grd = ctx.createLinearGradient(0, 0, 200, 0)
grd.addColorStop(0, 'red')
grd.addColorStop(1, 'white')
// Fill with gradient
ctx.setFillStyle(grd)
ctx.fillRect(10, 10, 150, 80)
ctx.draw()
}
圆圈
{
const ctx = wx.createCanvasContext('myCanvas')
// Create circular gradient
const grd = ctx.createCircularGradient(75, 50, 50)
grd.addColorStop(0, 'red')
grd.addColorStop(1, 'white')
// Fill with gradient
ctx.setFillStyle(grd)
ctx.fillRect(10, 10, 150, 80)
ctx.draw()
}
分包加载
主包 默认启动页面/TabBar 页面
整个小程序所有分包大小不超过 8M
单个分包/主包大小不能超过 2M
多团队共同开发时可以更好的解耦协作
使用分包
开发者通过在 app.json subpackages 字段声明项目分包结构:
"subpackages": [
{
"root": "packageA",
"pages": [
"pages/cat",
"pages/dog"
]
}, {
"root": "packageB",
"name": "pack2",
"pages": [
"pages/apple",
"pages/banana"
]
}
]
字段 类型 说明
root String 分包根目录
name String 分包别名,分包预下载时可以使用
pages StringArray 分包页面路径,相对与分包根目录
independent Boolean 分包是否是独立分包
打包原则
低版本兼容
由微信后台编译来处理旧版本客户端的兼容,后台会编译两份代码包,一份是分包后代码,另外一份是整包的兼容代码。
新客户端用分包,老客户端还是用的整包
独立分包
一个小程序中可以有多个独立分包。
小游戏不支持独立分包。
....> ...new...>用才回头先过...ykds
开发者通过在app.json的subpackages字段中对应的分包配置项中定义independent字段声明对应分包为独立分包。
(1)关于 getApp()
与普通分包不同,独立分包运行时,App 并不一定被注册,因此 getApp() 也不一定可以获得 App 对象:
由于独立分包中无法定义 App,小程序生命周期的监听可以使用 wx.onAppShow,wx.onAppHide 完成。
分包预下载
,通过在 app.json 增加 preloadRule 配置来控制。
lun add maxmin扩 primary second..replace sort
every day some
多线程 Worker
Worker 与主线程之间的数据传输,双方使用 Worker.postMessage() 来发送数据,Worker.onMessage() 来接收数据,传输的数据并不是直接共享,而是被复制的
app.json 中可配置 Worker 代码放置的目录
"workers": "workers"
后端 API
小程序还提供了一系列在后端服务器使用 HTTPS 请求调用的 API,帮助开发者在后台完成各类数据分析、管理和查询等操作。如 getAccessToken,code2Session 等
access_token
access_token 是小程序全局唯一后台接口调用凭据,
调用绝大多数后台接口时都需使用。
开发者可以通过 getAccessToken 接口获取并进行妥善保存
为了 access_token 的安全性,
后端 API 不能直接在小程序内通过 wx.request 调用,
即 api.weixin.qq.com 不能被配置为服务器域名。
开发者应在后端服务器使用getAccessToken获取 access_token,并调用相关 API;
请求参数说明
当API调用成功时,部分接口不会返回 errcode 和 errmsg,只有调用失败时才会返回。
消息推送
自定义 tabBar
getTabBar 接口,获取当前页面的自定义 tabBar 组件实例
app.json 中的 tabBar 项指定 custom 字段,同时其余 tabBar 相关配置也补充完整。
周期性更新
设置 TOKEN
第一次启动小程序时,调用 wx.setBackgroundFetchToken() 设置一个 TOKEN 字符串,可以跟用户态相关,会在后续微信客户端向开发者服务器请求时带上,便于给后者校验请求合法性。
App({
onLaunch() {
wx.setBackgroundFetchToken({
token: 'xxx'
})
}
})
微信客户端每隔 12 个小时才会发起一次请求,调试周期性更新功能会显得不太方便。