ofo至今還沒有微信小程序(很費解),每次用ofo都得去支付寶,很不方便,我用微信用的比較多,無意間在簡書上面看到某人寫了一個關(guān)于ofo的小程序,鏈接如下:給ofo小黃車擼一個微信小程序,不過數(shù)據(jù)都是模擬的,沒有數(shù)據(jù)庫,沒有后臺,這對于一個PHP(拍黃片)攻城獅來說,是可忍孰不可忍呀,剛剛學(xué)完七月老師的課程,受益匪淺,剛好自己動手做一個,說動手就動手,let's do it;
體驗版頁面
支付頁面
計費頁面
開鎖頁面
用車頁面
開鎖頁面
充值頁面
個人中心頁面
我的錢包頁面
首頁頁面
用戶表:
**user | CREATE TABLE `user` (**
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`openid` varchar(50) NOT NULL COMMENT '用戶的唯一標(biāo)識',
`create_time` int(11) DEFAULT NULL,
`delete_time` int(11) DEFAULT NULL,
`balance` decimal(60,2) NOT NULL COMMENT '余額',
`guarantee` decimal(60,2) NOT NULL COMMENT '保證金',
`update_time` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8 |
小黃車表:
**| bike | CREATE TABLE `bike` (**
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`latitude` float(11,6) NOT NULL COMMENT '經(jīng)度',
`is_show` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0未使用 1使用',
`longitude` float(11,6) NOT NULL COMMENT '緯度',
`password` int(11) NOT NULL COMMENT '單車密碼',
`type` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0正常,1故障',
`create_time` int(11) NOT NULL,
`update_time` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 |
故障分類表:
**| trouble_cate | CREATE TABLE `trouble_cate` (**
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL COMMENT '故障名稱',
`create_time` int(11) DEFAULT NULL,
`update_time` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8 |
故障記錄表:
**| trouble_record | CREATE TABLE `trouble_record` (**
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '用戶ID',
`bike_id` int(11) DEFAULT NULL COMMENT '單車ID',
`longitude` varchar(50) NOT NULL COMMENT '經(jīng)度',
`latitude` varchar(50) NOT NULL COMMENT '緯度',
`img` varchar(50) DEFAULT NULL COMMENT '上傳的圖片',
`remark` varchar(50) DEFAULT NULL COMMENT '備注',
`create_time` int(11) NOT NULL,
`update_time` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=29 DEFAULT CHARSET=utf8 |
充值表:
**| charge | CREATE TABLE `charge` (**
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL COMMENT '用戶ID',
`price` decimal(60,2) NOT NULL COMMENT '費用',
`type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '0為保證金 1為余額',
`create_time` int(11) NOT NULL,
`update_time` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=101 DEFAULT CHARSET=utf8 |
騎行記錄表:
**| record | CREATE TABLE `record` (**
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`bike_id` int(11) NOT NULL COMMENT '單車ID',
`user_id` int(11) NOT NULL COMMENT '用戶ID',
`end_time` int(11) NOT NULL COMMENT '結(jié)束時間',
`start_time` int(11) NOT NULL COMMENT '開始時間',
`total_price` decimal(10,0) NOT NULL COMMENT '總價格',
`start_long` varchar(50) NOT NULL COMMENT '開始經(jīng)度',
`start_lati` varchar(50) NOT NULL COMMENT '開始緯度',
`end_long` varchar(50) NOT NULL COMMENT '結(jié)束經(jīng)度',
`end_lati` varchar(50) NOT NULL COMMENT '結(jié)束緯度',
`create_time` int(11) NOT NULL,
`update_time` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=47 DEFAULT CHARSET=utf8 |
根據(jù)效果圖,很明顯我們知道肯定需要一個獲取單車信息的接口,接口代碼如下:
/**
* @return false|\PDOStatement|string|\think\Collection
* @throws BikeException
* 獲取單車的位置信息
*/
public function getBicyclePosition() {
$bikes = BikeModel::getBicyclePosition();
if(!$bikes) {
throw new BikeException();
}
return $bikes;
}
立即用車按鈕分析,首先我們需要先判斷有沒有登錄,登錄我們使用的是token令牌(后面會在個人中心登錄按鈕講下如何生成token令牌,如何利用tp5的緩存,使token令牌有有效期),如果令牌存在,我們還得判斷令牌是否有效,否則重新登錄,如果驗證通過,我們還得判斷這個用戶是否已經(jīng)有押金,如果沒有押金,跳到充值頁面去充值,否則跳轉(zhuǎn)到用車頁面,根據(jù)分析,我們需要一個驗證token是否有效的接口,接口代碼如下,
/**
* @return bool
* @throws TokenException
* 驗證token
*/
public function verifyToken() {
$token = Request::instance()->header('token');
$var = Cache::get($token);
if(!$var) {
throw new TokenException([
'msg'=>'token已經(jīng)過期',
'errorCode'=>10002
]);
}
return true;
}
我們還需要一個獲取用戶信息的接口,判斷是否有押金,接口代碼如下:
/**
* @return null|static
* @throws UserException
* 獲取用戶的信息
*/
public function getUserInfo(){
$uid = Token::getCurrentUid();
$user = UserModel::get($uid);
if(!$user) {
throw new UserException();
}
return $user;
}
故障按鈕分析:同樣的我們需要驗證是否登錄,登錄是否過期,否則我們跳轉(zhuǎn)到登錄頁面。(注意:我們需要把用戶的初始位置,記錄到小程序的緩存中,因為騎行記錄表需要記錄用戶的初始位置)
關(guān)于使用token令牌的好處,請自行百度,首先我先用一張圖來說明微信小程序如何獲取token:
根據(jù)效果圖,我們需要獲取token令牌接口,接口代碼如下:
/**
* @param $code
* @return array
* 獲取token
*/
public function getToken($code) {
(new TokenGet())->goCheck();
$user = new UserToken($code);
$token = $user->get();
return [
'token'=>$token
];
}
設(shè)置token的有效期,把token存儲在服務(wù)器端的緩存中,返回token,客戶端獲取到token,存儲到緩存中,雙向存儲token,以后每次訪問接口都攜帶token,更加安全,有效的防止有人偽造token獲取接口的信息
根據(jù)效果圖,點擊我的錢包按鈕需要跳轉(zhuǎn)到我的錢包頁面,我們需要一個獲取用戶信息的接口,接口代碼如下:
/**
* @return null|static
* @throws UserException
* 獲取用戶的信息
*/
public function getUserInfo(){
$uid = Token::getCurrentUid();
$user = UserModel::get($uid);
if(!$user) {
throw new UserException();
}
return $user;
}
退出登錄按鈕:我們需要刪除本地token,跳轉(zhuǎn)到登錄頁面
根據(jù)效果圖:我們需要一個充值的接口,因為是個人開發(fā),沒有商戶號,所以微信支付就沒有做,不過其實微信支付也并不難,附上微信支付的流程:
商戶系統(tǒng)和微信支付系統(tǒng)主要交互說明:
步驟1:用戶在商戶APP中選擇商品,提交訂單,選擇微信支付。
步驟2:商戶后臺收到用戶支付單,調(diào)用微信支付統(tǒng)一下單接口。參見【[統(tǒng)一下單API](https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_1)】。
步驟3:統(tǒng)一下單接口返回正常的prepay_id,再按簽名規(guī)范重新生成簽名后,將數(shù)據(jù)傳輸給APP。參與簽名的字段名為appid,partnerid,prepayid,noncestr,timestamp,package。注意:package的值格式為Sign=WXPay
步驟4:商戶APP調(diào)起微信支付。api參見本章節(jié)【[app端開發(fā)步驟說明](https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_5)】
步驟5:商戶后臺接收支付通知。api參見【[支付結(jié)果通知API](https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_7)】
步驟6:商戶后臺查詢支付結(jié)果。,api參見【[查詢訂單API](https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_2)】
這個接口需要注意的是,從哪個頁面過來的,從首頁過來的,應(yīng)該就是押金充值,從我的錢包頁面和支付頁面過來的,就應(yīng)該是余額充值,根據(jù)form不同,我們數(shù)據(jù)庫充值記錄表里面的type就不同,type為1代表余額充值,type為1為押金充值,接口代碼如下:
/**
* @param $guarantee
* 充值
*/
public function pay($from,$price) {
$type = 1;
if($from == 'index') {
$type = 0;
}else if($from == 'wallet' || $from == 'pay') {
$type = 1;
}
$uid = Token::getCurrentUid();
Db::startTrans();
try{
if($type == 1) {
$user = UserModel::get($uid);
$price = $price + $user->balance;
$result = new UserModel();
$res = $result->save(['balance'=>$price],['id'=>$uid]);
}else {
$res = UserModel::update(['guarantee'=>$price],['id'=>$uid]);
}
$rel = Charge::create([
'price'=>$price,
'type'=>$type,
'user_id'=>$uid
]);
if($rel && $res) {
Db::commit();
}
}catch (Exception $e) {
Db::rollback();
throw new UserException([
'msg'=>'充值失敗'
]);
}
}
根據(jù)效果圖,我們需要一個獲取單車密碼的接口,根據(jù)用戶輸入的ID,獲取單車的信息,如果is_show為1,服務(wù)器拋出自定義的異常,單車正在被使用,type為1,單車被報修,出現(xiàn)故障,不能使用,單車如果不存在,拋出異常,單車不存在。獲取到單車的密碼后,攜帶密碼和單車號到結(jié)果頁面,接口代碼如下:
/**
* @param $id
* @return array|false|\PDOStatement|string|\think\Model
* @throws BikeException
* 根據(jù)單車的ID獲取單車的信息
*/
public function getBikeByID($id) {
// (new IsMustBePostiveInt())->goCheck();
$bike = BikeModel::getBikeByID($id);
if(!$bike) {
throw new BikeException([
'msg'=>'該車牌號不存在'
]);
}
if($bike['is_show'] == 1){
throw new BikeException([
'msg'=>'此單車正在被使用',
'errorCode'=>10001
]);
}
if($bike['type'] == 1) {
throw new BikeException([
'msg'=>'此單車多次被報修,暫不可使用',
'errorCode'=>10002
]);
}
return $bike;
}
}
根據(jù)效果圖:計時開始時,我們需要把單車的使用狀態(tài)改變,改變?yōu)檎谑褂脿顟B(tài),接口代碼如下:
/**
* @param $id
* 修改單車的使用狀態(tài)
*/
public function updateBikeStatus($type = 0,$id) {
// (new IsMustBePostiveInt())->goCheck();
if($type == 0) {
//鎖定單車,單車在被使用中
$data = [
'is_show'=>1
];
}elseif ($type == 1) {
//釋放單車,單車恢復(fù)使用
$data = [
'is_show'=>0
];
}elseif ($type == 2) {
//單車出現(xiàn)故障
$data = [
'type'=>1
];
}elseif ($type == 3) {
//單車恢復(fù)正常
$data = [
'type'=>0
];
}
$res = \app\api\model\Bike::update($data,['id'=>$id]);
if($res) {
return true;
}else {
echo false;
}
}
根據(jù)效果圖,我們首先需要一個獲取故障分類名稱的接口,接口代碼如下:
/**
* @return false|\PDOStatement|string|\think\Collection
* 獲取問題的分類信息
*/
public function getTroubleCate() {
$res = new \app\api\model\TroubleCate();
$troubleCate = $res->select();
return $troubleCate;
}
然后提交的時候,我們需要一個記錄故障的接口,這個接口中,我們首先需要判斷,如果沒有選擇車牌損壞,則必須填寫車牌號,否則服務(wù)器返回自定義的異常,請輸入單車號,單車和故障很明顯是多對多的關(guān)系,我們在記錄的時候,還要寫到另外一張表中去,有記錄ID和分類ID組成的主鍵的表,同時我們根據(jù)單車的ID還得修改單車的狀態(tài),接口代碼如下:
public function recordTrouble($record) {
//分為兩種情況,車牌損壞,車牌未損壞
//如果有車牌號碼,先判斷單車是否存在,不存在,拋出異常,
//如果存在,寫到trouble_record表,根據(jù)trouble_record
//的id,還有trouble_id寫到bike_trouble表,多對多表,全部寫入成功之后,
//修改bike表的type值,用到事務(wù),要么失敗,要么成功
$bikeID = $record['inputValue']['num'];
//2代表車牌被損壞,看不到車牌號碼
if(!in_array(2,$record['checkboxValue'])) {
if($bikeID) {
$bike = new Bike();
$bike->getBikeByID($bikeID);
}else {
throw new BikeException([
'msg'=>'請輸入單車編號',
'errorCode'=>10003
]);
}
}
try {
Db::startTrans();
$address = $record['address'];
$uid = \app\api\service\Token::getCurrentUid();
$troubleRecord = new \app\api\model\TroubleRecord();
$troubleRecord->user_id=$uid;
$troubleRecord->bike_id=$bikeID;
$troubleRecord->longitude=$address['start_long'];
$troubleRecord->latitude=$address['start_lati'];
$troubleRecord->img=json_encode($record['picUrls']);
$troubleRecord->remark=$record['inputValue']['desc'];
//更新故障記錄表troubleRecord
$troubleRecord->save();
$resID = $troubleRecord->id;
$troublesID = $record['checkboxValue'];
$newArr = array();
foreach ($troublesID as $k=>$v) {
$newArr[$k]['trouble_id'] = $v;
$newArr[$k]['record_id'] = $resID;
}
$bikeTrouble = new BikeTrouble();
//更新故障表bikeTrouble表
$rel = $bikeTrouble->saveAll($newArr);
if($bikeID) {
//修改單車的狀態(tài),發(fā)送了故障
$bike = new Bike();
$bike->updateBikeStatus(2,$bikeID);
}
if($resID && $rel) {
Db::commit();
}
}catch (Exception $e) {
Db::rollback();
}
}
根據(jù)效果圖:我們需要一個記錄騎行的接口,這個接口中,這里有對多張表的操作,所以我們利用了tp的事務(wù)(注意:mysql數(shù)據(jù)引擎MyISAM不支持事務(wù)),提高數(shù)據(jù)庫數(shù)據(jù)的一致性,我們需要記錄用戶的開始地址,開始時間,結(jié)束地址,結(jié)束時間,總價格,用戶的id,單車的id等等,我們還需要修改用戶表的余額,同時修改小程序緩存的余額,關(guān)鍵點的是,我們還要再次獲取用戶的地址,及時修改單車的使用狀態(tài)和位置,便于其他用戶的使用,小黃車沒有GPS定位系統(tǒng),而是巧妙的利用了用戶的地址,這里我們看下小黃車的整個使用流程:
接口代碼如下:
/**
* @param $start_time
* @param $bikeID
* @param $end_time
* @param $start_long
* @param $start_lati
* @param $end_long
* @param $end_lati
* @param $price
* 用戶騎行后記錄到數(shù)據(jù)庫
*/
public function record($start_time,$bikeID,$end_time,$start_long,$start_lati,$end_long,$end_lati,$price) {
$uid = Token::getCurrentUid();
$data = [
'start_time'=>$start_time,
'end_time'=>$end_time,
'start_long'=>$start_long,
'start_lati'=>$start_lati,
'end_lati'=>$end_lati,
'end_long'=>$end_long,
'total_price'=>$price,
'user_id'=>$uid,
'bike_id'=>$bikeID
];
Db::startTrans();
try {
//創(chuàng)建記錄
$res = Record::create($data);
//修改用戶的余額
$user = new UserModel();
$userInfo = $user->find($uid);
$data = [
'balance'=>$userInfo->balance-$price
];
$rel = $user->save($data,['id'=>$uid]);
//修改小黃車的狀態(tài)和位置
$bikeData = [
'is_show'=>'0',
'latitude'=>$end_lati,
'longitude'=>$end_long
];
$rs = \app\api\model\Bike::update($bikeData,['id'=>$bikeID]);
if($res && $rel && $rs) {
echo 'success';
Db::commit();
}
}catch (Exception $e) {
Db::rollback();
}
}
到這里,ofo小程序的制作就到了尾聲了。開篇我們簡單進(jìn)行了數(shù)據(jù)庫的設(shè)計,然后一個一個頁面從頁面分析,到完成接口設(shè)計,分別響應(yīng)著不同的業(yè)務(wù)邏輯,有的頁面與頁面之間有數(shù)據(jù)往來,我們就通過跳轉(zhuǎn)頁面?zhèn)鲄⒒蛟O(shè)置本地存儲來將它們建立起聯(lián)系,環(huán)環(huán)相扣,構(gòu)建起了整個小程序的基本功能,使原本的ofo小程序有了靈魂。
首先感謝慕課網(wǎng)和慕課網(wǎng)的講師七月老師,微信小程序商城構(gòu)建全棧應(yīng)用這門課程對我一個還沒畢業(yè),還沒有什么工作經(jīng)驗的小白來說影響很大,改變了我對傳統(tǒng)互聯(lián)網(wǎng)的看法,前后端分離,使分工更加明確,后端工程師只要專注于數(shù)據(jù)和業(yè)務(wù),這個項目做完,使我對前后端分離理解深刻,注意代碼的復(fù)用性,實踐才是王道,這個項目采用了tp5框架,自定義了全局異常類,自定義驗證器,加深了我對AOP思想的理解,使用restful API設(shè)計接口,更加符合規(guī)范。
源碼在我的github主頁上面,需要的請移步下載github鏈接,如果喜歡,請給一個start,謝謝
工作日 8:30-12:00 14:30-18:00
周六及部分節(jié)假日提供值班服務(wù)