ofo至今(jin)還(huan)沒有(you)(you)微(wei)信(xin)小程序(xu)(xu)(很費解(jie)),每(mei)次用(yong)ofo都得去(qu)支付寶,很不(bu)方便,我用(yong)微(wei)信(xin)用(yong)的比較(jiao)多,無意間在(zai)簡書上面看(kan)到某人寫了(le)一(yi)(yi)個(ge)關于(yu)ofo的小程序(xu)(xu),鏈接如(ru)下:給ofo小黃車擼一(yi)(yi)個(ge)微(wei)信(xin)小程序(xu)(xu),不(bu)過數據(ju)都是模擬(ni)的,沒有(you)(you)數據(ju)庫(ku),沒有(you)(you)后臺,這對于(yu)一(yi)(yi)個(ge)PHP(拍黃片(pian))攻城獅來說(shuo),是可忍(ren)孰(shu)不(bu)可忍(ren)呀,剛剛學完七(qi)月老師(shi)的課程,受益匪(fei)淺,剛好自己動(dong)手做一(yi)(yi)個(ge),說(shuo)動(dong)手就(jiu)動(dong)手,let's do it;
體驗版頁面
支付頁面
計費頁面
開鎖頁面
用車頁面
開鎖頁面
充值頁面
個人中心頁面
我的錢包頁面
首頁頁面


用戶表:
**user | CREATE TABLE `user` (**
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`openid` varchar(50) NOT NULL COMMENT '用戶的(de)唯一標識(shi)',
`create_time` int(11) DEFAULT NULL,
`delete_time` int(11) DEFAULT NULL,
`balance` decimal(60,2) NOT NULL COMMENT '余(yu)額',
`guarantee` decimal(60,2) NOT NULL COMMENT '保證(zheng)金',
`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 '經度',
`is_show` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0未使(shi)用(yong) 1使(shi)用(yong)',
`longitude` float(11,6) NOT NULL COMMENT '緯度',
`password` int(11) NOT NULL COMMENT '單(dan)車密碼',
`type` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0正常,1故(gu)障',
`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 '故(gu)障名稱',
`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 '用戶(hu)ID',
`bike_id` int(11) DEFAULT NULL COMMENT '單車(che)ID',
`longitude` varchar(50) NOT NULL COMMENT '經度(du)',
`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 '單車(che)ID',
`user_id` int(11) NOT NULL COMMENT '用戶ID',
`end_time` int(11) NOT NULL COMMENT '結(jie)束(shu)時間',
`start_time` int(11) NOT NULL COMMENT '開(kai)始時間(jian)',
`total_price` decimal(10,0) NOT NULL COMMENT '總價格',
`start_long` varchar(50) NOT NULL COMMENT '開(kai)始經(jing)度',
`start_lati` varchar(50) NOT NULL COMMENT '開(kai)始緯(wei)度',
`end_long` varchar(50) NOT NULL COMMENT '結束(shu)經度',
`end_lati` varchar(50) NOT NULL COMMENT '結(jie)束緯度',
`create_time` int(11) NOT NULL,
`update_time` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=47 DEFAULT CHARSET=utf8 |

根據效(xiao)果圖(tu),很明顯(xian)我(wo)們(men)知(zhi)道肯定需要一個獲取單車信息的接口,接口代碼(ma)如下:
/**
* @return false|\PDOStatement|string|\think\Collection
* @throws BikeException
* 獲取單車的位置(zhi)信(xin)息(xi)
*/
public function getBicyclePosition() {
$bikes = BikeModel::getBicyclePosition();
if(!$bikes) {
throw new BikeException();
}
return $bikes;
}
立即用(yong)車(che)按鈕分(fen)析,首先我們需要先判(pan)(pan)斷(duan)(duan)有(you)沒有(you)登錄,登錄我們使(shi)用(yong)的是(shi)token令(ling)(ling)牌(pai)(后面會在個人中心登錄按鈕講下如何生成token令(ling)(ling)牌(pai),如何利(li)用(yong)tp5的緩存,使(shi)token令(ling)(ling)牌(pai)有(you)有(you)效(xiao)期),如果令(ling)(ling)牌(pai)存在,我們還得判(pan)(pan)斷(duan)(duan)令(ling)(ling)牌(pai)是(shi)否(fou)有(you)效(xiao),否(fou)則(ze)重新登錄,如果驗證(zheng)通(tong)過(guo),我們還得判(pan)(pan)斷(duan)(duan)這(zhe)個用(yong)戶是(shi)否(fou)已(yi)經有(you)押金,如果沒有(you)押金,跳(tiao)到充值(zhi)頁面去充值(zhi),否(fou)則(ze)跳(tiao)轉到用(yong)車(che)頁面,根(gen)據(ju)分(fen)析,我們需要一個驗證(zheng)token是(shi)否(fou)有(you)效(xiao)的接(jie)口(kou),接(jie)口(kou)代(dai)碼如下,
/**
* @return bool
* @throws TokenException
* 驗證token
*/
public function verifyToken() {
$token = Request::instance()->header('token');
$var = Cache::get($token);
if(!$var) {
throw new TokenException([
'msg'=>'token已經過期',
'errorCode'=>10002
]);
}
return true;
}
我們還需要一個獲取用戶信息(xi)的(de)接口(kou),判斷是否有(you)押(ya)金,接口(kou)代碼如下:
/**
* @return null|static
* @throws UserException
* 獲取(qu)用戶的信(xin)息
*/
public function getUserInfo(){
$uid = Token::getCurrentUid();
$user = UserModel::get($uid);
if(!$user) {
throw new UserException();
}
return $user;
}
故障按鈕分析:同樣的(de)(de)我(wo)們需(xu)要驗證是否(fou)(fou)登錄,登錄是否(fou)(fou)過期,否(fou)(fou)則我(wo)們跳轉到登錄頁面(mian)。(注意:我(wo)們需(xu)要把(ba)用(yong)(yong)戶(hu)的(de)(de)初(chu)始位置(zhi),記錄到小程(cheng)序的(de)(de)緩存(cun)中,因為騎(qi)行(xing)記錄表(biao)需(xu)要記錄用(yong)(yong)戶(hu)的(de)(de)初(chu)始位置(zhi))
關于使用token令牌的好處,請自行百度,首先我先用一張圖來說明微信小程序如何獲取token:

根據效果(guo)圖,我們(men)需(xu)要獲取token令(ling)牌接口(kou),接口(kou)代(dai)碼(ma)如下:
/**
* @param $code
* @return array
* 獲取token
*/
public function getToken($code) {
(new TokenGet())->goCheck();
$user = new UserToken($code);
$token = $user->get();
return [
'token'=>$token
];
}
設置token的(de)有效期,把token存(cun)(cun)儲(chu)(chu)在服務(wu)器(qi)端的(de)緩存(cun)(cun)中(zhong),返回token,客(ke)戶(hu)端獲(huo)取(qu)到(dao)token,存(cun)(cun)儲(chu)(chu)到(dao)緩存(cun)(cun)中(zhong),雙向存(cun)(cun)儲(chu)(chu)token,以后(hou)每次訪(fang)問(wen)接口都攜帶token,更加安(an)全,有效的(de)防止有人(ren)偽造(zao)token獲(huo)取(qu)接口的(de)信息

根據效果圖,點擊我(wo)的(de)錢包按(an)鈕(niu)需(xu)要跳轉到我(wo)的(de)錢包頁(ye)面,我(wo)們(men)需(xu)要一個獲取用戶信息的(de)接口,接口代(dai)碼(ma)如(ru)下:
/**
* @return null|static
* @throws UserException
* 獲取(qu)用戶(hu)的(de)信(xin)息
*/
public function getUserInfo(){
$uid = Token::getCurrentUid();
$user = UserModel::get($uid);
if(!$user) {
throw new UserException();
}
return $user;
}
退(tui)出登錄按鈕:我們需要刪除本地token,跳轉到登錄頁面
根據效果圖:我們需要一個充值的(de)接(jie)口,因為(wei)是(shi)個人開發,沒有商(shang)戶(hu)號,所以微信支付就沒有做,不(bu)過(guo)其實微信支付也并不(bu)難,附上微信支付的(de)流(liu)程:
商戶系(xi)統(tong)和微信支付(fu)系(xi)統(tong)主(zhu)要(yao)交互說明(ming):
步驟1:用(yong)戶在商戶APP中(zhong)選(xuan)擇(ze)商品,提交訂單,選(xuan)擇(ze)微信支付。
步(bu)驟2:商戶(hu)后臺收到用戶(hu)支付單,調(diao)用微信支付統一(yi)下(xia)單接口。參見【[統一(yi)下(xia)單API](https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_1)】。
步(bu)驟3:統一下單接口(kou)返回正(zheng)常的prepay_id,再按簽名規范重新生成簽名后,將數據傳輸(shu)給(gei)APP。參與簽(qian)名的字段(duan)名為(wei)appid,partnerid,prepayid,noncestr,timestamp,package。注意(yi):package的值格式為Sign=WXPay
步驟(zou)4:商戶APP調起微信支付。api參(can)見本章節【[app端開發步驟說明(ming)](https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=8_5)】
步驟5:商(shang)戶后臺接收支付通(tong)知。api參見【[支付(fu)結果通知API](https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_7)】
步驟6:商(shang)戶后臺查詢(xun)支付(fu)結果。,api參見【[查詢訂(ding)單API](https://pay.weixin.qq.com/wiki/doc/api/app/app.php?chapter=9_2)】
這個接口(kou)需要(yao)注意(yi)的(de)是,從哪個頁面(mian)(mian)過來的(de),從首(shou)頁過來的(de),應該就(jiu)(jiu)是押(ya)金充(chong)值(zhi)(zhi),從我的(de)錢包頁面(mian)(mian)和支付(fu)頁面(mian)(mian)過來的(de),就(jiu)(jiu)應該是余額充(chong)值(zhi)(zhi),根據(ju)form不同,我們數據(ju)庫充(chong)值(zhi)(zhi)記(ji)錄表里面(mian)(mian)的(de)type就(jiu)(jiu)不同,type為1代表余額充(chong)值(zhi)(zhi),type為1為押(ya)金充(chong)值(zhi)(zhi),接口(kou)代碼如下:
/**
* @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'=>'充值(zhi)失敗'
]);
}
}

根(gen)據效果(guo)(guo)圖,我們需要(yao)一個獲取單(dan)車(che)(che)密碼(ma)的接口,根(gen)據用戶輸入的ID,獲取單(dan)車(che)(che)的信息,如(ru)果(guo)(guo)is_show為(wei)1,服務器(qi)拋出自(zi)定義的異常(chang),單(dan)車(che)(che)正在被使用,type為(wei)1,單(dan)車(che)(che)被報修,出現故障(zhang),不(bu)能使用,單(dan)車(che)(che)如(ru)果(guo)(guo)不(bu)存在,拋出異常(chang),單(dan)車(che)(che)不(bu)存在。獲取到(dao)單(dan)車(che)(che)的密碼(ma)后,攜帶密碼(ma)和單(dan)車(che)(che)號到(dao)結(jie)果(guo)(guo)頁面,接口代碼(ma)如(ru)下(xia):
/**
* @param $id
* @return array|false|\PDOStatement|string|\think\Model
* @throws BikeException
* 根據單車(che)的(de)ID獲(huo)取(qu)單車(che)的(de)信息
*/
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'=>'此(ci)單(dan)車正在被使用',
'errorCode'=>10001
]);
}
if($bike['type'] == 1) {
throw new BikeException([
'msg'=>'此(ci)單車多次被報(bao)修,暫不可使用',
'errorCode'=>10002
]);
}
return $bike;
}
}

根據效果(guo)圖:計(ji)時開始時,我們(men)需要把單(dan)車的使用狀態(tai)改變,改變為正(zheng)在(zai)使用狀態(tai),接口(kou)代(dai)碼(ma)如下:
/**
* @param $id
* 修改單車的使用狀(zhuang)態
*/
public function updateBikeStatus($type = 0,$id) {
// (new IsMustBePostiveInt())->goCheck();
if($type == 0) {
//鎖定單車,單車在被使用中
$data = [
'is_show'=>1
];
}elseif ($type == 1) {
//釋放(fang)單(dan)車,單(dan)車恢(hui)復(fu)使用
$data = [
'is_show'=>0
];
}elseif ($type == 2) {
//單(dan)車(che)出現故障
$data = [
'type'=>1
];
}elseif ($type == 3) {
//單車(che)恢(hui)復正常
$data = [
'type'=>0
];
}
$res = \app\api\model\Bike::update($data,['id'=>$id]);
if($res) {
return true;
}else {
echo false;
}
}
根據(ju)效果圖,我們首先需(xu)要一個獲取故障分(fen)類名稱的接(jie)口(kou),接(jie)口(kou)代碼如下:
/**
* @return false|\PDOStatement|string|\think\Collection
* 獲取問題的分類信息
*/
public function getTroubleCate() {
$res = new \app\api\model\TroubleCate();
$troubleCate = $res->select();
return $troubleCate;
}
然后(hou)提交的(de)(de)時(shi)候,我(wo)們需要一個(ge)記(ji)錄(lu)(lu)故(gu)(gu)障的(de)(de)接(jie)口(kou),這個(ge)接(jie)口(kou)中,我(wo)們首先(xian)需要判斷,如果沒有選擇車(che)(che)(che)牌損壞,則必須填寫車(che)(che)(che)牌號(hao),否則服務器(qi)返回自定義的(de)(de)異(yi)常,請輸入單車(che)(che)(che)號(hao),單車(che)(che)(che)和(he)(he)故(gu)(gu)障很明顯是多對多的(de)(de)關(guan)系(xi),我(wo)們在記(ji)錄(lu)(lu)的(de)(de)時(shi)候,還(huan)要寫到另(ling)外一張表(biao)中去,有記(ji)錄(lu)(lu)ID和(he)(he)分類ID組成的(de)(de)主鍵的(de)(de)表(biao),同時(shi)我(wo)們根據單車(che)(che)(che)的(de)(de)ID還(huan)得修改單車(che)(che)(che)的(de)(de)狀態(tai),接(jie)口(kou)代(dai)碼如下:
public function recordTrouble($record) {
//分(fen)為兩種情況,車(che)牌損壞(huai),車(che)牌未損壞(huai)
//如果有(you)車牌號碼,先判斷(duan)單車是否存在,不(bu)存在,拋出異(yi)常,
//如(ru)果(guo)存在,寫到trouble_record表,根(gen)據trouble_record
//的id,還有(you)trouble_id寫到bike_trouble表,多對(dui)多表,全部寫入成(cheng)功之后,
//修改bike表(biao)的type值(zhi),用到事務,要(yao)么失(shi)敗,要(yao)么成功
$bikeID = $record['inputValue']['num'];
//2代表車(che)牌(pai)被損壞(huai),看不到車(che)牌(pai)號碼
if(!in_array(2,$record['checkboxValue'])) {
if($bikeID) {
$bike = new Bike();
$bike->getBikeByID($bikeID);
}else {
throw new BikeException([
'msg'=>'請輸入單車編號(hao)',
'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();
//更新故障表(biao)bikeTrouble表(biao)
$rel = $bikeTrouble->saveAll($newArr);
if($bikeID) {
//修改單車(che)的狀態,發送了(le)故(gu)障
$bike = new Bike();
$bike->updateBikeStatus(2,$bikeID);
}
if($resID && $rel) {
Db::commit();
}
}catch (Exception $e) {
Db::rollback();
}
}

根據效果圖:我們需要一個記錄騎行的接口,這個接口中,這里有對多張表的操作,所以我們利用了tp的事務(注意:mysql數據引擎MyISAM不支持事務),提高數據庫數據的一致性,我們需要記錄用戶的開始地址,開始時間,結束地址,結束時間,總價格,用戶的id,單車的id等等,我們還需要修改用戶表的余額,同時修改小程序緩存的余額,關鍵點的是,我們還要再次獲取用戶的地址,及時修改單車的使用狀態和位置,便于其他用戶的使用,小黃車沒有GPS定位系統,而是巧妙的利用了用戶的地址,這里我們看下小黃車的整個使用流程:

接口(kou)代(dai)碼如下:
/**
* @param $start_time
* @param $bikeID
* @param $end_time
* @param $start_long
* @param $start_lati
* @param $end_long
* @param $end_lati
* @param $price
* 用戶騎(qi)行后記錄到數據庫(ku)
*/
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 {
//創建記錄
$res = Record::create($data);
//修改用戶的余額(e)
$user = new UserModel();
$userInfo = $user->find($uid);
$data = [
'balance'=>$userInfo->balance-$price
];
$rel = $user->save($data,['id'=>$uid]);
//修改(gai)小黃(huang)車的狀態和位置
$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小程序的制作就到了尾聲了。開篇我們簡單進行了數據庫的設計,然后一個一個頁面從頁面分析,到完成接口設計,分別響應著不同的業務邏輯,有的頁面與頁面之間有數據往來,我們就通過跳轉頁面傳參或設置本地存儲來將它們建立起聯系,環環相扣,構建起了整個小程序的基本功能,使原本的ofo小程序有了靈魂。
首先感謝慕課網和慕課網的講師七月老師,微信小程序商城構建全棧應用這門課程對我一個還沒畢業,還沒有什么工作經驗的小白來說影響很大,改變了我對傳統互聯網的看法,前后端分離,使分工更加明確,后端工程師只要專注于數據和業務,這個項目做完,使我對前后端分離理解深刻,注意代碼的復用性,實踐才是王道,這個項目采用了tp5框架,自定義了全局異常類,自定義驗證器,加深了我對AOP思想的理解,使用restful API設計接口,更加符合規范。
源碼在我(wo)的(de)github主頁上(shang)面,需要(yao)的(de)請移步(bu)下載(zai)github鏈接,如(ru)果(guo)喜歡,請給一(yi)個start,謝(xie)謝(xie)