添加功能模塊
接下來我們將介紹如何添加如圖所示的邀請功能模塊。
邀請功能介紹及原理
- 每個(gè)注冊用戶有一個(gè)邀請鏈接,鏈接中包含了邀請人的user_id,例如 https://bla.cn/ai/101
- 受邀者訪問這個(gè)鏈接時(shí)系統(tǒng)會記錄一個(gè)cookie ai_inviter=邀請人user_id
- 受邀者注冊時(shí)讀取這個(gè)cookie,將邀請人和被邀請人記錄到 ai_invite 表
- 被邀請人購買會員時(shí)邀請人獲得獎勵,獎勵記錄到 ai_invite_rewards 表
注意
為了避免和其插件表名沖突,AI系統(tǒng)相關(guān)的表格一律以ai_開頭
創(chuàng)建項(xiàng)目
php webman app-plugin:create ai_invite
建表
一鍵菜單生成后臺
給新建的兩個(gè)表分別生成菜單
ai_inviteb表
控制器 /plugin/ai_invite/app/admin/controller/AiInviteController.php
模型 /plugin/ai_invite/app/model/AiInvite.php
ai_invite_rewards表
控制器 /plugin/ai_invite/app/admin/controller/AiInviteRewardController.php
模型 /plugin/ai_invite/app/model/AiInviteReward.php
生成結(jié)果
刷新后能看到生成的菜單如下
以上操作會生成兩個(gè)模型AiInvite
AiInviteReward
,開發(fā)者可以在后續(xù)代碼中直接使用。
邀請鏈接設(shè)計(jì)及實(shí)現(xiàn)
邀請鏈接類似 http://bla.cn/ai/101 ,其中101是邀請人的用戶ID,進(jìn)入這個(gè)鏈接后實(shí)際上就是顯示AI的主頁(用戶不會感到任何區(qū)別),但是會記錄一個(gè)cookie ai_inviter,值為101。
為了不入侵現(xiàn)有AI系統(tǒng)代碼,我們需要使用中間件來做這個(gè)事情。
新建中間件 plugin/ai_invite/app/middleware/Invite.php
內(nèi)容如下
<?php
namespace plugin\ai_invite\app\middleware;
use Webman\MiddlewareInterface;
use Webman\Http\Response;
use Webman\Http\Request;
class Invite implements MiddlewareInterface
{
public function process(Request $request, callable $handler) : Response
{
$response = $handler($request);
if ($request->route && $aiInviter = $request->route->param('ai_inviter')) {
if (!$request->cookie('ai_inviter')) {
$response->cookie('ai_inviter', $aiInviter, 3600 * 24 * 365, '/');
}
}
return $response;
}
}
打開 plugin/ai_invite/config/route.php
添加以下路由
<?php
use plugin\ai_invite\app\middleware\Invite;
use plugin\ai\app\controller\IndexController;
use Webman\Route;
Route::any('/ai/{ai_inviter:\d+}', [IndexController::class, 'index'])->middleware(Invite::class);
這樣當(dāng)訪問 http://bla.cn/ai/101
時(shí)會記錄一個(gè)cookie ai_inviter,值為101。
通過事件系統(tǒng)實(shí)現(xiàn)相關(guān)功能
系統(tǒng)有新用戶注冊時(shí)會觸發(fā)一個(gè)user.register
事件,用戶購買會員時(shí)會觸發(fā)一個(gè)ai.payment.success
事件,左側(cè)圖標(biāo)菜單渲染事件為ai.menu.list
,我們可以通過監(jiān)聽這些事件來實(shí)現(xiàn)對應(yīng)的功能,并且不會入侵現(xiàn)有AI系統(tǒng)源碼。
如果你想要了解系統(tǒng)的其它事件請參考 事件列表
創(chuàng)建 plugin/ai_invite/config/event.php
內(nèi)容如下
<?php
use plugin\ai\app\event\data\EventData;
use plugin\ai\app\model\AiModel;
use plugin\ai_invite\app\model\AiInvite;
use plugin\ai_invite\app\model\AiInviteReward;
use plugin\ai_invite\app\service\Invite;
use plugin\user\app\model\User;
use plugin\ai\api\User as ApiUser;
return [
// 用戶注冊時(shí)觸發(fā)
'user.register' => [
function (User $user) {
$request = request();
if (!$request) {
return;
}
$inviter = $request->cookie('ai_inviter');
if ($inviter && is_numeric($inviter) && User::find($inviter)) {
$aiInvite = new AiInvite();
$aiInvite->inviter = $inviter;
$aiInvite->invitee = $user->id;
$aiInvite->percent = Invite::getSetting()['percent'] ?? 10;
$aiInvite->save();
}
}
],
// 在ai系統(tǒng)支付時(shí)觸發(fā)
'ai.payment.success' => [
function ($paymentData) {
$userId = $paymentData->userId;
$data = $paymentData->data;
$aiInvite = AiInvite::where('invitee', $userId)->first();
if (!$aiInvite) {
return;
}
$percent = 0.1;
$inviter = $aiInvite->inviter;
$reward = new AiInviteReward;
$reward->inviter = $inviter;
$reward->invitee = $userId;
$reward->percent = $percent;
$reward->data = json_encode($data);
$reward->amount = round($data['price'] * $percent, 2);
$reward->type = 'recharge';
$reward->save();
// 自動給邀請人增加余額
$modelTypes = AiModel::pluck('type')->toArray();
$data['days'] = isset($data['months']) ? $data['months'] * 30 : $data['days'];
$data['days'] = ceil($data['days'] * $percent);
unset($data['months']);
foreach ($data as $key => $value) {
if (in_array($key, $modelTypes)) {
$data[$key] = ceil($value * $percent);
}
}
ApiUser::addBalanceByPlanData($inviter, $data);
}
],
// 渲染左側(cè)圖標(biāo)菜單時(shí)觸發(fā)
'ai.menu.list' => [
function (EventData $object) {
$data = $object->data;
$data['invite'] = [
'enabled' => true, // 是否啟用
'title' => '邀請好友', // 標(biāo)題
'icon' => [
'light' => '<i class="bi bi-share"></i>', // 明亮主題下的圖標(biāo)
'dark' => '<i class="bi bi-share"></i>', // 暗黑主題下的圖標(biāo)
'active' => '<i class="bi bi-share-fill"></i>' // 被選中后的圖標(biāo)
],
'url' => '/app/ai_invite', // iframe url 地址
'mobile' => true, // 是否在移動端顯示圖標(biāo)菜單
];
$object->data = $data;
}
]
];
開發(fā) /app/ai_invite 頁面
plugin/ai_invite/app/controller/IndexController.php
控制器內(nèi)容如下
<?php
namespace plugin\ai_invite\app\controller;
use plugin\ai_invite\app\model\AiInvite;
use plugin\ai_invite\app\model\AiInviteReward;
use plugin\ai_invite\app\service\Invite;
use plugin\user\api\User;
use support\Request;
class IndexController
{
// 首頁
public function index(Request $request)
{
if (!session('user')) {
return redirect('/app/ai/user/login?redirect=' . urlencode($request->uri()));
}
$userId = session('user.uid') ?? session('user.id');
$totalAmount = AiInviteReward::where('inviter', $userId)->sum('amount');
$totalRewords = AiInviteReward::where('inviter', $userId)->count();
$totalInvitees = AiInvite::where('inviter', $userId)->count('invitee');
return view('index/index', [
'totalAmount' => $totalAmount,
'totalRewords' => $totalRewords,
'totalInvitees' => $totalInvitees,
]);
}
// 加載邀請獎勵數(shù)據(jù)
public function invitees()
{
$userId = session('user.uid') ?? session('user.id');
$items = AiInviteReward::where('inviter', $userId)->orderBy('id')->get();
$userIdArray = array_unique($items->pluck('invitee')->toArray());
$users = User::whereIn('id', $userIdArray)->get()->keyBy('id');
$data = [];
$typeMap = [
'recharge' => '充值獎勵',
'register' => '注冊獎勵',
];
foreach ($items as $item) {
$user = $users[$item->invitee];
$data[] = [
'id' => $user->id,
'nickname' => $user->nickname,
'avatar' => $user->avatar,
'amount' => $item->amount,
'created_at' => $item->created_at,
'type' => $typeMap[$item->type],
];
}
return json([
'code' => 0,
'msg' => 'ok',
'data' => $data,
]);
}
}
plugin/ai_invite/app/view/index/index.html
頁面內(nèi)容如下
<!doctype html>
<html lang="zh-cn">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<link rel="shortcut icon" href="/favicon.ico" />
<link href="/app/ai/css/bootstrap.min.css?v=5.3" rel="stylesheet">
<link href="/app/ai/css/app.css?v=<?=$css_version??1?>" rel="stylesheet">
<script src="/app/ai/js/jquery.min.js"></script>
<script src="/app/ai/js/bootstrap.bundle.min.js?v=5.3"></script>
<title>webman AI - 邀請</title>
</head>
<body data-bs-theme="light">
<div class="header">邀請好友</div>
<div class="container-fluid p-4 overflow-scroll" style="height: calc(100% - 45px)">
<div class="row">
<div class="col-12 pt-2" id="app">
<div class="rounded white-bg py-4 px-3">
<div class="d-flex justify-content-center">
<div class="d-flex justify-content-around f16" style="max-width:400px; min-width: 380px;">
<div class="d-flex flex-column justify-content-center align-items-center">
<div class="text-secondary mb-2">總收益</div>
<div>¥<?=$totalAmount?></div>
</div>
<div class="d-flex flex-column justify-content-center align-items-center">
<div class="text-secondary mb-2">獎勵次數(shù)</div>
<div><?=$totalRewords?></div>
</div>
<div class="d-flex flex-column justify-content-center align-items-center">
<div class="text-secondary mb-2">邀請人數(shù)</div>
<div><?=$totalInvitees?></div>
</div>
</div>
</div>
<div class="d-flex flex-column align-items-center justify-content-center mt-4">
<div>
<span class="me-2">邀請鏈接</span>
<input ref="inputElement" class="form-control me-2 d-inline-block border text-secondary mb-1" type="text" v-model="inviteUrl" readonly style="width:18em">
<button class="btn btn-primary" @click="copyToClipboard">復(fù)制</button>
</div>
<div class="mt-3 text-success">
你將獲得好友充值的<b>10%</b>的獎勵,獎勵將自動兌換為相應(yīng)的vip會員額度。
</div>
</div>
</div>
<div class="row" v-cloak>
<div v-for="invitee in invitees" class="col-12 col-sm-6 col-md-6 col-lg-4 col-xl-3">
<div class="white-bg shadow-sm mt-3 rounded p-3">
<div class="d-flex align-items-center justify-content-between role position-relative">
<div class="d-flex align-items-center">
<img :src="invitee.avatar" class="avatar me-2"/>
<div>
<div>{{invitee.nickname}}</div>
<div class="text-secondary-sm">{{invitee.type}}</div>
</div>
</div>
<div class="text-secondary">
{{invitee.amount}}¥
</div>
</div>
<div class="mt-3 d-flex justify-content-between align-items-center">
<span class="text-secondary-sm">已自動兌換</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- vue -->
<script type="text/javascript" src="/app/ai/js/vue.global.js"></script>
<script>
const App = {
data() {
return {
invitees: [],
inviteUrl: '',
}
},
mounted() {
this.loadInvitees();
this.inviteUrl = location.origin + '/ai/' + window.parent.ai.loginUser.userid;
},
methods: {
copyToClipboard() {
const inputElement = this.$refs.inputElement;
const item = new ClipboardItem({ 'text/plain': new Blob([inputElement.value], { type: 'text/plain' }) });
navigator.clipboard.write([item]).then(() => {
webman.success('復(fù)制成功');
}).catch((error) => {
console.error('復(fù)制失敗', error);
});
},
loadInvitees() {
$.ajax({
url: "/app/ai_invite/index/invitees",
success: (res) => {
if (res.code) {
return alert(res.msg);
}
this.invitees = res.data;
}
});
}
}
}
Vue.createApp(App).mount('#app');
</script>
<script src="/app/user/js/webman.js"></script>
</body>
</html>
最終效果類似如下
install.sql
如果你想發(fā)布插件給其他人使用,需要在插件目錄下創(chuàng)建一個(gè)install.sql
文件,里面的SQL會在插件安裝或升級時(shí)執(zhí)行,用來初始化數(shù)據(jù)庫表結(jié)構(gòu)及升級修改表結(jié)構(gòu)。
plugin/ai_invite/install.sql
CREATE TABLE `ai_invite` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`created_at` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
`updated_at` datetime DEFAULT NULL COMMENT '更新時(shí)間',
`inviter` int DEFAULT NULL COMMENT '邀請人',
`invitee` int DEFAULT NULL COMMENT '受邀人',
`percent` int DEFAULT '10' COMMENT '獎勵百分比',
PRIMARY KEY (`id`),
UNIQUE KEY `invitee` (`invitee`),
KEY `inviter` (`inviter`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='邀請';
CREATE TABLE `ai_invite_rewards` (
`id` int NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`created_at` datetime DEFAULT NULL COMMENT '創(chuàng)建時(shí)間',
`updated_at` datetime DEFAULT NULL COMMENT '更新時(shí)間',
`inviter` int DEFAULT NULL COMMENT '邀請人',
`invitee` int DEFAULT NULL COMMENT '受邀人',
`amount` decimal(8,2) DEFAULT NULL COMMENT '獎勵',
`percent` int DEFAULT NULL COMMENT '百分比',
`data` text COLLATE utf8mb4_general_ci COMMENT '數(shù)據(jù)',
`type` enum('register','recharge') COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'recharge' COMMENT '類型',
PRIMARY KEY (`id`),
KEY `inviter` (`inviter`),
KEY `invitee` (`invitee`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='邀請獎勵';
注意
每個(gè)語句以;結(jié)束特別注意
如果你的插件后續(xù)版本需要修改表結(jié)構(gòu),只能通過追加的方式將alter table語句放再install.sql末尾,千萬不要直接修改建表語句,否則老用戶無法實(shí)現(xiàn)升級。
這部分的說明參考 webman應(yīng)用插件自動導(dǎo)入數(shù)據(jù)庫
發(fā)布插件并獲取收益
參考 發(fā)布插件