Shu-How Zの小窝

Loading...

微信小程序商城构建全栈应用

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 { 申请令牌
} 获取请求参数 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"; { } 模板 不支持 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 { } 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}} } 11-20商品详情页面四 只能一个img 详细全 id=11 banner 贵妃笑 product 同一个数据绑定里面 console.log 调试 AppData 数据绑定 查看 销后保障 简单 自己 扩展 html { 数量 {{productCounts}} 加入购物车 有货 缺货 {{product.name}} ${{product.price}} {{item}} } 11-21商品分类页面一 分类 pages/category/category 布局 左 点击左加载右 右 上图片 下product模板 手机web 比电脑web 简单多 前端 多写 网页 交互 在加一个view 布局 不用上下 左右 css 不同人不同 没有固定 写法 布局 css css html { {{item.name}} } 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 { {{item.name}} } 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 { {{cartTotalCounts}} 数量 {{productCounts}} 加入购物车 有货 缺货 {{product.name}} ${{product.price}} {{item}} } 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 { {{userInfo.nickName}} 地址管理 +添加地址 我的订单 订单编号: {{item.order_no}} {{item.snap_name}} {{item.total_count}}件商品 待付款 已付款 已发货 实付:¥{{item.total_price}} 付款 } 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.order_no}} {{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 { {{userInfo.nickName}} 地址管理 +添加地址 我的订单 订单编号: {{item.order_no}} {{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: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:微信里面标记 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 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}} 点击 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}} {{item}} } 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 属性,作为模板的名字。然后在
posted @ 2024-12-14 16:04  KooTeam  阅读(54)  评论(0)    收藏  举报