Compare commits

...
1 Commits
Author SHA1 Message Date
atshike@foxmail.com 016c0f4886 移除套餐/支付 2025-12-19 21:51:44 +08:00
42 changed files with 65 additions and 1767 deletions
-16
View File
@@ -122,14 +122,6 @@ const apiRouter: IMenu[] = [
title: '会员管理'
}
},
{
path: '/super/vipSetMeal',
name: 'vipSetMeal',
component: '/super/vipSetMeal/index',
meta: {
title: 'vip套餐'
}
},
]
},
{
@@ -169,14 +161,6 @@ const apiRouter: IMenu[] = [
meta: {
title: '域名管理'
}
},
{
path: '/super/payRecord',
name: 'payRecord',
component: '/super/payRecord/index',
meta: {
title: '付款记录'
}
}
]
},
-1
View File
@@ -25,7 +25,6 @@ const { state, sizeChangeHandle, currentChangeHandle } = useCurd({
>
<el-table-column prop="id" label="ID" width="60"></el-table-column>
<el-table-column prop="username" label="手机号"></el-table-column>
<el-table-column prop="agent_package.name" label="当前套餐"> </el-table-column>
<el-table-column prop="credit" label="剩余积分"></el-table-column>
<el-table-column prop="accumulate_credit" label="累计充值积分"> </el-table-column>
<el-table-column prop="commission" label="剩余佣金" width="120"> </el-table-column>
+10 -11
View File
@@ -71,9 +71,8 @@ const getHomeData = async (type: any) => {
member_count: res.member_count,
agent_count: res.agent_count,
user_count: res.user_count,
pay_vip: res.pay_vip,
pay_agent: res.pay_agent,
pay_count: res.pay_count,
card_count: res.card_count,
uv_count: res.uv_count,
}
// 用户增长趋势
if (res.user_growth.length > 0) {
@@ -116,24 +115,24 @@ onMounted(() => {
</div>
<img src="@/assets/admin-home/use-count.png" alt="" />
</div>
<!-- 会员数 -->
<!-- 卡片数量 -->
<div class="item-box vip-count-bg">
<div class="item-box-l">
<span class="name">支付会员数</span>
<span class="name">卡片数量</span>
<div>
<span class="value">{{ dataValue?.pay_count || 0 }}</span>
<span></span>
<span class="value">{{ dataValue?.card_count || 0}}</span>
<span></span>
</div>
</div>
<img src="@/assets/admin-home/use-count.png" alt="" />
</div>
<!-- 会员收入 -->
<!-- 卡片过期数量 -->
<div class="item-box vip-inp-bg">
<div class="item-box-l">
<span class="name">购买会员收入</span>
<span class="name">UV次数</span>
<div>
<span class="value">{{ dataValue?.pay_vip }}</span>
<span></span>
<span class="value">{{ dataValue?.uv_count || 0}}</span>
<span></span>
</div>
</div>
<img src="@/assets/admin-home/use-count.png" alt="" />
-148
View File
@@ -1,148 +0,0 @@
<script setup lang="ts">
import { configStore } from '@/stores'
import { data_get } from '@/utils'
import PayButton from '@/views/home/components/PayButton.vue'
</script>
<template>
<table class="equity-table">
<thead>
<tr>
<th>权益/版本</th>
<th :class="`th-${index + 1}`" v-for="(item, index) in configStore.package" :key="index">
<div class="margin-b-5">{{ item.name }}</div>
<div>{{ item.price }}/</div>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="td-1 padding-l-20">每月访问数量UV上限</td>
<td :class="`td-${index + 2}`" v-for="(item, index) in configStore.package" :key="index">
{{ data_get(item, 'config.uv_limit') }}
</td>
</tr>
<tr>
<td class="td-1 padding-l-20">链接创建数量上限同时</td>
<td :class="`td-${index + 2}`" v-for="(item, index) in configStore.package" :key="index">
{{ data_get(item, 'config.count_limit') }}
</td>
</tr>
<tr>
<td class="td-1 padding-l-20">自建小程序池可创建数量</td>
<td :class="`td-${index + 2}`" v-for="(item, index) in configStore.package" :key="index">
{{ data_get(item, 'config.min_count_limit') }}/
</td>
</tr>
<tr>
<td class="td-1 padding-l-20">链接保留时长</td>
<td :class="`td-${index + 2}`" v-for="(item, index) in configStore.package" :key="item.id">
永久
</td>
</tr>
<tr>
<td class="td-1 padding-l-20">支持的类型</td>
<td :class="`td-${index + 2}`" v-for="(item, index) in configStore.package" :key="index">
<div v-if="data_get(item, 'config.allow_type.CLI_QR')">草料二维码</div>
<div v-if="data_get(item, 'config.allow_type.KING_DOC')">金山文档</div>
<div v-if="data_get(item, 'config.allow_type.WORK_WECHAT')">企业微信</div>
<div v-if="data_get(item, 'config.allow_type.LANDING_MINI')">微信二维码</div>
<div v-if="data_get(item, 'config.allow_type.MINI_PROGRAM')">自定义小程序</div>
</td>
</tr>
<tr>
<td class="td-1 padding-l-20">官方小程序池使用</td>
<td :class="`td-${index + 2}`" v-for="(item, index) in configStore.package" :key="index">
<span v-if="data_get(item, 'config.pre_min')">支持</span>
<span v-else class="red">不支持</span>
</td>
</tr>
<tr>
<td class="td-1 padding-l-20">数据统计/分析</td>
<td :class="`td-${index + 2}`" v-for="(item, index) in configStore.package" :key="item.id">
支持
</td>
</tr>
<tr>
<td class="td-1 padding-l-20">自定义落地页</td>
<td :class="`td-${index + 2}`" v-for="(item, index) in configStore.package" :key="index">
<span v-if="data_get(item, 'config.cur_index')">支持</span>
<span v-else class="red">不支持</span>
</td>
</tr>
<tr>
<td class="td-1 padding-l-20">自建小程序池下架自动检测</td>
<td :class="`td-${index + 2}`" v-for="(item, index) in configStore.package" :key="index">
<span v-if="data_get(item, 'config.min_disabled_check')">支持</span>
<span v-else class="red">不支持</span>
</td>
</tr>
<tr>
<td class="td-1 padding-l-20">免费技术协助</td>
<td :class="`td-${index + 2}`" v-for="(item, index) in configStore.package" :key="item.id">
支持
</td>
</tr>
<tr>
<td class="td-1"></td>
<td :class="`td-${index + 2}`" v-for="(item, index) in configStore.package" :key="item.id">
<PayButton v-if="item.price > 0" type="pay-vip" :info="item">
<el-button type="primary">立即开通/续费</el-button>
</PayButton>
<el-button v-else>免费</el-button>
</td>
</tr>
</tbody>
</table>
</template>
<style scoped lang="scss">
table,
tr,
td,
th {
border: 1px solid #eaebee;
border-collapse: collapse;
}
.equity-table {
width: 100%;
}
thead tr {
height: 80px;
font-size: 16px;
.th-1 {
background-color: #d7e9ff;
}
.th-2 {
background-color: #ffebcc;
}
.th-3 {
background-color: #ffe6e6;
}
}
tbody tr {
td {
padding-top: 8px;
padding-bottom: 8px;
}
font-size: 16px;
text-align: center;
.td-1 {
width: 260px;
text-align: left;
}
.td-2 {
background-color: #f9fcff;
}
.td-3 {
background-color: #fffdf8;
}
.td-4 {
background-color: #fff9fa;
}
}
.red {
color: #ff0000;
}
</style>
+12 -114
View File
@@ -5,7 +5,6 @@ import { userStore } from '@/stores'
import ChangePassword from '@/views/home/components/ChangePassword.vue'
import Welcome from '@/views/home/components/Welcome.vue'
import { data_get, setClipboard } from '@/utils'
import BuyVip from '@/views/home/components/BuyVip.vue'
import { getNotify } from '@/api/super'
// 获取概况数据
const dataList = ref<any>({})
@@ -25,13 +24,7 @@ const copyCode = () => {
var path = window.location.pathname
setClipboard(root + path + '#/register?code=' + code)
}
const success = ref(false)
// 开通会员
const buyVipVisible = ref(false)
const tiyan = () => {
success.value = false
localStorage.removeItem('register')
}
const notify = ref({} as any)
const noticeShow = ref(false)
onMounted(() => {
@@ -43,21 +36,15 @@ onMounted(() => {
noticeShow.value = true
}
})
if (localStorage.getItem('register')) {
success.value = true
}
})
</script>
<template>
<div class="home_container" v-if="initFlag">
<!-- 公告 -->
<!-- <HomeNotice :notices="dataList.notices" />-->
<el-row :gutter="10">
<el-col :xs="24" :sm="16">
<el-row :gutter="12">
<el-col :xs="24" :sm="24">
<el-row :gutter="10">
<el-col :xs="24"><Welcome /></el-col>
<el-col :xs="22"><Welcome /></el-col>
</el-row>
<el-row :gutter="10" style="margin-top: 10px">
<el-col :xs="24">
@@ -69,23 +56,17 @@ onMounted(() => {
<el-row :gutter="10">
<el-col :xs="24" :sm="12">
<div class="home-card" style="background-color: #f6fafe; --num-color: #2b6bff">
<div class="home-card-title">本月链接访问UV</div>
<div class="home-card-title">链接访问UV</div>
<div class="home-card-warp">
<div class="home-card-item">
<div class="home-card-num">{{ dataList.used_uv ?? 0 }}</div>
<div class="home-num-title">已使用UV次数</div>
<div class="home-num-title">UV次数</div>
</div>
<div class="home-card-item">
<div class="home-card-num">
{{ data_get(userStore.userInfo, 'vip_package.config.uv_limit', 0) }}
{{ dataList.month_uv ?? 0 }}
</div>
<div class="home-num-title">会员套餐UV次数</div>
</div>
<div class="home-card-item">
<div class="home-card-num">
{{ data_get(userStore.userInfo, 'vip_package.config.uv_limit', 0) - (dataList.used_uv ?? 0) }}
</div>
<div class="home-num-title">剩余可访问UV次数</div>
<div class="home-num-title">本月UV次数</div>
</div>
</div>
</div>
@@ -95,20 +76,14 @@ onMounted(() => {
<div class="home-card-title">已创建数量</div>
<div class="home-card-warp">
<div class="home-card-item">
<div class="home-card-num"> {{ data_get(userStore.userInfo, 'link_total', 0) }}</div>
<div class="home-num-title">总链接</div>
<div class="home-card-num"> {{ data_get(userStore.userInfo, 'link_created', 0) }}</div>
<div class="home-num-title">卡片数量</div>
</div>
<div class="home-card-item">
<div class="home-card-num">
{{ data_get(userStore.userInfo, 'link_created', 0) }}
{{ data_get(userStore.userInfo, 'link_expire', 0) }}
</div>
<div class="home-num-title">已创建</div>
</div>
<div class="home-card-item">
<div class="home-card-num">
{{ data_get(userStore.userInfo, 'link_amount', 0) }}
</div>
<div class="home-num-title">未创建</div>
<div class="home-num-title">到期卡片</div>
</div>
</div>
</div>
@@ -118,40 +93,6 @@ onMounted(() => {
</el-col>
</el-row>
</el-col>
<el-col :xs="24" :sm="8">
<div class="vip-info bgw-box">
<div class="right-h margin-b-20">
<img class="margin-right-5" src="@/assets/home/vip-icon.png" alt="" />
<span class="title">会员信息</span>
</div>
<div class="right-m margin-b-20">
{{
userStore.userInfo.site_name
}}作为一款私域引流工具凭借其强大的功能和灵活的应用场景成为了推广营销领域的热门选择它支持从抖音直接跳转到个人微信微信群小程序以及企业微信客服有效打通了不同平台之间的壁垒实现了流量的高效转化和利用推广营销必备
</div>
<div class="right-f margin-b-20">
<span class="margin-right-10">引流平台功能</span>
<img class="margin-right-10" src="@/assets/home/q-wx.png" alt="" />
<img class="margin-right-10" src="@/assets/home/wx.png" alt="" />
</div>
<div class="right-s margin-b-20">
<span class="margin-right-10">当前会员套餐</span>
<span>{{ data_get(userStore.userInfo, 'vip_package.name', '未开通会员') }}</span>
</div>
<div class="right-s margin-b-20">
<span class="margin-right-10">会员到期日期</span>
<span>{{ userStore.userInfo.end_at }}</span>
</div>
<el-row :gutter="10">
<el-col :xs="24" :sm="24">
<el-button class="btn" type="primary" @click="buyVipVisible = true">开通会员/续费会员</el-button>
<el-dialog title="开通会员" width="1000px" append-to-body v-model="buyVipVisible">
<BuyVip />
</el-dialog>
</el-col>
</el-row>
</div>
</el-col>
</el-row>
<el-row :gutter="10">
@@ -173,35 +114,6 @@ onMounted(() => {
</div>
</el-col>
</el-row>
<el-row class="m-t-10">
<el-col :span="24">
<div class="bgw-box">
<div class="right-h margin-b-20">
<img class="margin-right-5" src="@/assets/home/vip-icon.png" alt="" />
<span class="title">会员订单记录</span>
</div>
<el-table
height="249"
:data="dataList.vip_logs"
:header-cell-style="{
'text-align': 'center',
'background-color': '#F1F1F1',
height: '50px',
color: '#333'
}"
:cell-style="{ padding: '0', 'text-align': 'center', height: '50px', color: '#333' }"
>
<el-table-column prop="vip_package.name" label="类型"></el-table-column>
<el-table-column prop="amount" label="金额"></el-table-column>
<el-table-column prop="id" label="支付方式">
<template v-slot:default>微信</template>
</el-table-column>
<el-table-column prop="start_at" label="生效时间"></el-table-column>
<el-table-column prop="end_at" label="到期时间"></el-table-column>
</el-table>
</div>
</el-col>
</el-row>
<el-dialog :title="notify.title" :show-close="true" :width="700" append-to-body v-model="noticeShow">
<div class="notify">
@@ -212,20 +124,6 @@ onMounted(() => {
</div>
</el-dialog>
<el-dialog title="" :show-close="false" :width="500" append-to-body v-model="success">
<div class="successCard">
<div style="flex: 1">
<h2 class="margin-b-10">注册成功</h2>
<h3>恭喜您获得1天体验会员100uv访问量</h3>
</div>
<div style="text-align: center">
<img class="margin-b-20" src="@/assets/home/success.png" alt="" />
</div>
</div>
<div style="text-align: right; margin-right: 30px">
<el-button type="primary" @click="tiyan">立即体验</el-button>
</div>
</el-dialog>
<el-dialog :title="notify.title" :show-close="true" :width="700" append-to-body v-model="noticeShow">
<div class="notify">
<div class="notify" v-html="notify.content"></div>
+1 -1
View File
@@ -14,7 +14,7 @@
:row-key="state.primaryKey"
>
<el-table-column prop="id" label="ID" width="60"></el-table-column>
<el-table-column prop="username" label="手机号"></el-table-column>
<el-table-column prop="username" label="号"></el-table-column>
<el-table-column prop="created_at" label="注册时间"></el-table-column>
<template #empty>
-9
View File
@@ -99,15 +99,6 @@ const commissionShow = (user_id: any) => {
>
<el-table-column prop="id" label="ID" width="60"></el-table-column>
<el-table-column prop="username" label="手机号" width="120"> </el-table-column>
<el-table-column prop="agent_package.name" label="当前套餐"> </el-table-column>
<!--<el-table-column prop="agent_level.name" label="级别">
<template #default="{ row }">
<el-button type="primary" link :icon="Edit" @click="changeForm(row, 'level_id')">
<span v-if="row.agent_level && row.agent_level.name">{{ row.agent_level.name }}</span>
<span v-else></span>
</el-button>
</template>
</el-table-column>-->
<el-table-column prop="credit" label="剩余积分">
<template #default="{ row }">
<el-button type="primary" link :icon="Edit" @click="changeForm(row, 'credit')">
-139
View File
@@ -1,139 +0,0 @@
<script setup lang="ts">
import useCurd from '@/hooks/curd'
const { state, resetInput, research, sizeChangeHandle, currentChangeHandle } = useCurd({
url: '/payments'
})
</script>
<template>
<el-card>
<div class="header">
<div class="header-left">
<el-form>
<el-row :gutter="20">
<el-col :span="3">
<el-form-item label="状态">
<el-select v-model="state.queryForm.status" placeholder="请选择">
<el-option key="1" label="已下单" value="1"></el-option>
<el-option key="2" label="已支付" value="2"></el-option>
<el-option key="3" label="已取消" value="3"></el-option>
</el-select>
</el-form-item>
</el-col>
<el-col :span="3">
<el-form-item label="付款人">
<el-input
v-model="state.queryForm.payer_username"
placeholder="输入付款人"
></el-input>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="下单时间">
<el-date-picker
v-model="state.queryForm.created_at"
placeholder="下单时间"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="5">
<el-form-item label="支付时间">
<el-date-picker
v-model="state.queryForm.success_at"
placeholder="支付时间"
type="daterange"
value-format="YYYY-MM-DD HH:mm:ss"
></el-date-picker>
</el-form-item>
</el-col>
<el-col :span="5">
<el-button type="primary" @click="research">搜索</el-button>
<el-button @click="resetInput">重置</el-button>
</el-col>
</el-row>
</el-form>
</div>
</div>
<el-table
:data="state.dataList"
:header-cell-style="{
'text-align': 'center',
'background-color': '#F1F1F1',
height: '50px',
color: '#333'
}"
:cell-style="{ padding: '0', 'text-align': 'center', height: '50px', color: '#333' }"
v-loading="state.listLoading"
:row-key="state.primaryKey"
>
<el-table-column prop="id" label="ID"></el-table-column>
<el-table-column prop="no" label="编号" width="200"></el-table-column>
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag v-if="row.status === 1">已下单</el-tag>
<el-tag v-if="row.status === 2" type="success" effect="dark">已支付</el-tag>
<el-tag v-if="row.status === 3" type="info" effect="dark">已取消</el-tag>
</template>
</el-table-column>
<el-table-column prop="job" label="套餐">
<template #default="{ row }">
<el-tag v-if="row.job === 'App\\Jobs\\PayAgent'" type="warning">
{{ state.meta && state.meta['agent'][row.attach.agent_id] }}
</el-tag>
<el-tag v-if="row.job === 'App\\Jobs\\PayVip' && row.attach.vip_id" type="success">
{{ state.meta && state.meta['vip'][row.attach.vip_id] }}
</el-tag>
<el-tag v-if="row.job === 'App\\Jobs\\PayCredit'" type="danger">充值积分</el-tag>
<span v-else></span>
</template>
</el-table-column>
<el-table-column prop="amount" label="金额"> </el-table-column>
<el-table-column prop="payer.username" label="付款人">
<template #default="{ row }">
<span v-if="row.payer && row.payer.username"> {{ row.payer.username }}</span>
<span v-else style="color: #999">{{ row.attach.username }}</span>
</template>
</el-table-column>
<el-table-column prop="payer.type" label="付款人类型">
<template #default="{ row }">
<el-tag v-if="row.payer && row.payer.type === 1">会员</el-tag>
<el-tag v-if="row.payer && row.payer.type === 2" type="warning">代理</el-tag>
<el-tag v-if="row.payer && row.payer.type === 3">管理员</el-tag>
</template>
</el-table-column>
<el-table-column prop="success_at" label="支付时间"></el-table-column>
<el-table-column prop="created_at" label="下单时间"></el-table-column>
<template #empty>
<el-empty></el-empty>
</template>
</el-table>
<!-- 分页 -->
<el-pagination
:current-page="state.page"
:total="state.total"
:page-sizes="state.pageSizes"
:page-size="state.limit"
background
layout="total, sizes, prev, pager, next"
@size-change="sizeChangeHandle"
@current-change="currentChangeHandle"
/>
</el-card>
</template>
<style scoped lang="scss">
.header {
display: flex;
justify-content: space-between;
.header-left {
flex: 1;
}
}
:deep(.el-form) {
margin-bottom: 15px;
}
</style>
@@ -1,69 +0,0 @@
<script setup lang="ts">
import { ref, watch } from 'vue'
import { ApiSaveSet } from '@/api/comment'
import { ElMessage } from 'element-plus'
import { configStore } from '@/stores'
const props = defineProps<{
data: any
}>()
const webFormData = ref({
wechat_pay_app_id: '',
wechat_pay_mch_id: '',
wechat_pay_secret_key: '',
wechat_pay_private_cert: '',
wechat_pay_certificate: ''
})
watch(
() => props.data,
() => {
webFormData.value.wechat_pay_app_id = props.data.wechat_pay_app_id
webFormData.value.wechat_pay_mch_id = props.data.wechat_pay_mch_id
webFormData.value.wechat_pay_secret_key = props.data.wechat_pay_secret_key
webFormData.value.wechat_pay_private_cert = props.data.wechat_pay_private_cert
webFormData.value.wechat_pay_certificate = props.data.wechat_pay_certificate
},
{
deep: true,
immediate: true
}
)
const clickSubmit = () => {
ApiSaveSet(webFormData.value).then(() => {
ElMessage.success('保存成功')
configStore.refreshConfig()
})
}
</script>
<template>
<el-card>
<div style="width: 500px">
<div>
<div class="margin-b-10" style="margin-top: 20px">微信商户appId</div>
<el-input v-model="webFormData.wechat_pay_app_id" placeholder="请输入微信商户appId"></el-input>
</div>
<div>
<div class="margin-b-10" style="margin-top: 20px">微信商户号</div>
<el-input v-model="webFormData.wechat_pay_mch_id" placeholder="请输入微信商户号"></el-input>
</div>
<div>
<div class="margin-b-10" style="margin-top: 20px">微信支付密钥</div>
<el-input v-model="webFormData.wechat_pay_secret_key" placeholder="请输入微信支付密钥"></el-input>
</div>
<div>
<div class="margin-b-10" style="margin-top: 20px">微信支付私钥证书</div>
<el-input v-model="webFormData.wechat_pay_private_cert" placeholder="请输入微信支付私钥证书"></el-input>
</div>
<div>
<div class="margin-b-10" style="margin-top: 20px">微信支付公钥证书</div>
<el-input v-model="webFormData.wechat_pay_certificate" placeholder="请输入微信支付公钥证书"></el-input>
</div>
<div class="m-b-20 text-center" style="margin-top: 60px">
<el-button type="primary" size="large" @click="clickSubmit">保存</el-button>
</div>
</div>
</el-card>
</template>
<style scoped lang="scss"></style>
@@ -1,69 +0,0 @@
<script setup lang="ts">
import { ApiVipList } from '@/api/super'
import { onMounted, ref, watch } from 'vue'
import { ApiSaveSet } from '@/api/comment'
import { ElMessage } from 'element-plus'
import { configStore } from '@/stores'
const props = defineProps<{
data: any
}>()
const formData = ref({
is_give_vip: false,
give_vip_id: undefined,
give_vip_days: 0,
})
watch(
() => props.data,
() => {
formData.value.is_give_vip = props.data.is_give_vip
formData.value.give_vip_id = props.data.give_vip_id
formData.value.give_vip_days = props.data.give_vip_days
},
{
deep: true,
immediate: true
}
)
const vipPackages = ref<any>([])
const clickSubmit = () => {
ApiSaveSet(formData.value).then(() => {
ElMessage.success('保存成功')
configStore.refreshConfig()
})
}
onMounted(() => {
ApiVipList().then((res: any) => {
vipPackages.value = vipPackages.value.concat(res)
})
})
</script>
<template>
<el-card>
<div style="width: 500px">
<div>
<div class="margin-b-10" style="margin-top: 20px">是否开启赠送会员</div>
<el-switch style="width: 100%" v-model="formData.is_give_vip"></el-switch>
</div>
<div>
<div class="margin-b-10" style="margin-top: 20px">注册赠送会员</div>
<el-select v-model="formData.give_vip_id" placeholder="注册赠送会员" style="width: 100%">
<el-option v-for="vip in vipPackages" :key="vip.id" :label="vip.name" :value="vip.id" />
</el-select>
</div>
<div>
<div class="margin-b-10" style="margin-top: 20px">注册赠送天数</div>
<el-input-number style="width: 100%" v-model="formData.give_vip_days" :min="1" :max="365" />
</div>
<div class="m-b-20 text-center" style="margin-top: 60px">
<el-button type="primary" size="large" @click="clickSubmit">保存</el-button>
</div>
</div>
</el-card>
</template>
<style scoped lang="scss"></style>
@@ -4,8 +4,6 @@ import { ApiGetSetALL } from '@/api/comment'
import Web from '@/views/super/setting/components/web.vue'
import CodeSetUp from '@/views/super/setting/components/codeSetUp.vue'
import Pay from '@/views/super/setting/components/pay.vue'
import Sales from '@/views/super/setting/components/sales.vue'
const formData = ref<any>({})
@@ -27,12 +25,6 @@ const tab = ref('web')
<el-tab-pane label="验证码配置" name="code">
<CodeSetUp :data="formData"></CodeSetUp>
</el-tab-pane>
<el-tab-pane label="支付配置" name="pay">
<Pay :data="formData"></Pay>
</el-tab-pane>
<el-tab-pane label="注册赠送会员" name="sales">
<Sales :data="formData"></Sales>
</el-tab-pane>
</el-tabs>
</el-card>
</div>
-7
View File
@@ -104,13 +104,6 @@ const vipPackageChange = (data: any, detail?: any) => {
<el-tag v-if="row.type == 3" type="danger">管理员</el-tag>
</template>
</el-table-column>
<el-table-column prop="vip_package.name" label="当前套餐">
<template #default="{ row }">
<el-button type="primary" link :icon="Edit" @click="changeForm(row, 'vip_id')">
{{ row.vip_package && row.vip_package.name }}
</el-button>
</template>
</el-table-column>
<el-table-column prop="start_at" label="生效时间"></el-table-column>
<el-table-column prop="end_at" label="过期时间">
<template #default="{ row }">
@@ -1,114 +0,0 @@
<script setup lang="ts">
import type { FormInstance, FormRules } from 'element-plus'
import useForm from '@/hooks/form'
import { ref } from 'vue'
import type { vipConfig } from '@/models/link'
const formRef = ref<FormInstance>()
const { formData, formLoading } = useForm({
formRef,
defVal(detail) {
return {
name: detail?.name, // 套餐名称
price: detail?.price ? detail?.price : 0, // 价格
level: detail?.level ? detail?.level : 0, // 等级
config: detail?.config
? detail?.config
: ({
pre_min: false,
min_disabled_check: false,
support: false,
cur_index: false,
allow_type: {
MINI_PROGRAM: false,
KING_DOC: false,
CLI_QR: false,
WORK_WECHAT: false,
LANDING_MINI: false,
QR_QQ: false
}
} as vipConfig) // 权限配置
}
}
})
// 表单校验
const rules: FormRules = {
name: [{ required: true, message: '请输入套餐名称', trigger: 'blur' }],
price: [{ required: true, message: '请输入套餐价格', trigger: 'blur' }],
level: [{ required: true, message: '请填写等级', trigger: 'blur' }]
}
</script>
<template>
<el-form :model="formData" :rules="rules" ref="formRef" v-loading="formLoading" label-width="180px">
<el-form-item prop="name" label="套餐名称">
<el-input v-model="formData.name" placeholder="输入套餐名称"></el-input>
</el-form-item>
<el-form-item prop="price" label="价格/月">
<el-input v-model="formData.price" placeholder="0.00"></el-input>
</el-form-item>
<el-form-item prop="level" label="等级">
<el-input-number v-model="formData.level" placeholder="0"></el-input-number>
</el-form-item>
<h3>权限配置</h3>
<el-divider></el-divider>
<el-form-item :rules="[{ required: true, message: '请输入链接数量限制', trigger: 'blur' }]" label="链接数量限制">
<el-input-number v-model="formData.config.count_limit" placeholder="0"></el-input-number>
</el-form-item>
<!-- <el-form-item-->
<!-- :rules="[{ required: true, message: '请输入小圆码数量限制', trigger: 'blur' }]"-->
<!-- label="小圆码数量限制"-->
<!-- >-->
<!-- <el-input-number v-model="formData.config.yuanma_limit" placeholder="0"></el-input-number>-->
<!-- </el-form-item>-->
<!-- <el-form-item-->
<!-- :rules="[{ required: true, message: '请输入抖音自动回复卡片数量限制', trigger: 'blur' }]"-->
<!-- label="抖音自动回复卡片"-->
<!-- >-->
<!-- <el-input-number v-model="formData.config.douyin_reply_limit" placeholder="0"></el-input-number>-->
<!-- </el-form-item>-->
<el-form-item :rules="[{ required: true, message: '请输入UV限制', trigger: 'blur' }]" label="UV限制">
<el-input-number v-model="formData.config.uv_limit" placeholder="0"></el-input-number>
</el-form-item>
<el-form-item
:rules="[{ required: true, message: '请输入自建小程序池可创建数量', trigger: 'blur' }]"
label="自建小程序池可创建数量"
>
<el-input-number v-model="formData.config.min_count_limit" placeholder="0"></el-input-number>
</el-form-item>
<el-form-item :rules="[{ required: true, message: '请选择创建链接类型', trigger: 'blur' }]" label="创建链接类型">
<el-checkbox v-model="formData.config.allow_type.MINI_PROGRAM" :label="true">跳转到小程序</el-checkbox>
<el-checkbox v-model="formData.config.allow_type.LANDING_MINI" :label="true">跳转到微信二维码</el-checkbox>
<el-checkbox v-model="formData.config.allow_type.WORK_WECHAT" :label="true">跳转到企业微信</el-checkbox>
<el-checkbox v-model="formData.config.allow_type.KING_DOC" :label="true">跳转到金山文档</el-checkbox>
<el-checkbox v-model="formData.config.allow_type.CLI_QR" :label="true">跳转到草料二维码</el-checkbox>
<el-checkbox v-model="formData.config.allow_type.QR_QQ" :label="true">跳转到腾讯优码</el-checkbox>
</el-form-item>
<el-form-item :rules="[{ required: true, message: '请选择链接保留时长', trigger: 'blur' }]" label="链接保留时长">
<el-checkbox :model-value="true" disabled>永久</el-checkbox>
</el-form-item>
<el-form-item
:rules="[{ required: true, message: '请选择官方小程序池使用', trigger: 'blur' }]"
label="官方小程序池使用"
>
<el-checkbox v-model="formData.config.pre_min" :label="true">支持</el-checkbox>
</el-form-item>
<el-form-item
:rules="[{ required: true, message: '请选择自建小程序池下架自动检测', trigger: 'blur' }]"
label="自建小程序池下架自动检测"
>
<el-checkbox v-model="formData.config.min_disabled_check" :label="true">支持</el-checkbox>
</el-form-item>
<el-form-item :rules="[{ required: true, message: '请选择免费技术协助', trigger: 'blur' }]" label="免费技术协助">
<el-checkbox v-model="formData.config.support" :label="true">支持</el-checkbox>
</el-form-item>
<el-form-item :rules="[{ required: true, message: '请选择数据统计/分析', trigger: 'blur' }]" label="数据统计/分析">
<el-checkbox :model-value="true" disabled>支持</el-checkbox>
</el-form-item>
<el-form-item :rules="[{ required: true, message: '请选择自定义落地页', trigger: 'blur' }]" label="自定义落地页">
<el-checkbox v-model="formData.config.cur_index" :label="true">支持</el-checkbox>
</el-form-item>
</el-form>
</template>
<style scoped lang="scss"></style>
-120
View File
@@ -1,120 +0,0 @@
<script setup lang="ts">
import { Plus, Edit } from '@element-plus/icons-vue'
import useCurd from '@/hooks/curd'
import FormModal from '@/components/FormModal.vue'
import { ref } from 'vue'
import CreateOrLook from '@/views/super/vipSetMeal/components/create-or-look.vue'
const {
state,
showForm,
submitForm,
deleteHandle,
query,
inlineEdit
} = useCurd({
url: '/vip-packages',
openPage: false
})
// 行内编辑
// 表格排序switch和按钮的切换
const isShow = () => {
flag.value = false
}
// 实现表格排序及数值变化
const flag = ref(true)
const handleSort = async (row: any) => {
await inlineEdit(row.id, 'level', row.level)
await query()
flag.value = true
}
</script>
<template>
<el-card>
<div class="header-right">
<el-button type="primary" :icon="Plus" @click="showForm">新增套餐</el-button>
</div>
<!-- 表格 -->
<el-table
:data="state.dataList"
:header-cell-style="{
'text-align': 'center',
'background-color': '#F1F1F1',
height: '50px',
color: '#333'
}"
:cell-style="{ padding: '0', 'text-align': 'center', height: '50px', color: '#333' }"
v-loading="state.listLoading"
:row-key="state.primaryKey"
>
<el-table-column prop="name" label="套餐名称"></el-table-column>
<el-table-column prop="price" label="价格/月"> </el-table-column>
<el-table-column prop="level" label="等级">
<template #default="{ row }">
<div v-if="flag === false">
<el-input-number v-model="row.level"></el-input-number>
<el-button type="primary" link @click="flag = true">取消</el-button>
<el-button type="primary" link @click="handleSort(row)">确认</el-button>
</div>
<el-button v-else type="primary" link :icon="Edit" @click="isShow">
{{ row.level }}
</el-button>
</template>
</el-table-column>
<el-table-column label="权限配置">
<template #default="{ row }">
<div>链接数量{{ row.config?.count_limit }}</div>
<div>UV限制{{ row.config?.uv_limit }}</div>
</template>
</el-table-column>
<el-table-column prop="created_at" label="创建时间"> </el-table-column>
<el-table-column prop="" label="操作" fixed="right">
<template #default="{ row }">
<el-button type="primary" link @click="showForm(row)">编辑</el-button>
<el-button type="primary" link @click="deleteHandle(row.id)">删除</el-button>
</template>
</el-table-column>
<template #empty>
<el-empty></el-empty>
</template>
</el-table>
</el-card>
<!-- 添加标签 -->
<form-modal
v-if="state.formVisible === true"
v-model:visible="state.formVisible"
:loading="state.formLoading"
:detail="state.detailData"
@submit="submitForm"
type="dialog"
width="1000"
:title="state.detailData?.id ? '编辑' : '新增'"
cancelBtnName="取消"
okBtnName="确定"
>
<create-or-look></create-or-look>
</form-modal>
</template>
<style scoped lang="scss">
.top_card {
margin-bottom: 20px;
height: 72px;
}
//.header {
// display: flex;
// justify-content: space-between;
// .header-left {
// flex: 1;
// }
//}
.header-right {
margin-bottom: 20px;
}
:deep(.el-form) {
margin-bottom: 15px;
}
</style>
-40
View File
@@ -67,21 +67,6 @@ class SystemInit extends Command
'value' => '',
'desc' => '阿里短信签名',
],
[
'slug' => 'give_vip_days',
'value' => '3',
'desc' => '注册赠送会员有效期/天',
],
[
'slug' => 'give_vip_id',
'value' => '1',
'desc' => '赠送套餐',
],
[
'slug' => 'is_give_vip',
'value' => '1',
'desc' => '开启注册赠送会员',
],
[
'slug' => 'mail_from_address',
'value' => '',
@@ -142,31 +127,6 @@ class SystemInit extends Command
'value' => '蚂蚁快链-智慧引流工具',
'desc' => '网站名称',
],
[
'slug' => 'wechat_pay_app_id',
'value' => '',
'desc' => '微信商户appId',
],
[
'slug' => 'wechat_pay_certificate',
'value' => '',
'desc' => '微信支付公钥证书',
],
[
'slug' => 'wechat_pay_mch_id',
'value' => '',
'desc' => '微信商户号',
],
[
'slug' => 'wechat_pay_private_cert',
'value' => '',
'desc' => '微信支付私钥证书',
],
[
'slug' => 'wechat_pay_secret_key',
'value' => '',
'desc' => '微信支付密钥',
],
];
}
}
-66
View File
@@ -1,66 +0,0 @@
<?php
namespace App\Console\Commands;
use App\Enums\VipStatus;
use App\Models\User;
use App\Models\VipLogs;
use Illuminate\Console\Command;
class VipExpired extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'app:vip-expired';
/**
* The console command description.
*
* @var string
*/
protected $description = '处理vip套餐的过期和生效';
/**
* Execute the console command.
*/
public function handle()
{
// 已生效的,要过期掉, 未生效的,要生效
VipLogs::query()
->where(function ($query) {
$query->where('status', VipStatus::ACTIVE)
->where('end_at', '<=', now());
})
->orWhere(function ($query) {
$query->where('status', VipStatus::PENDING)
->where('start_at', '<=', now());
})
->chunkById(30, function ($logs) {
foreach ($logs as $log) {
if ($log->status === VipStatus::ACTIVE) {
// 生效中的要过期
$log->status = VipStatus::EXPIRED;
$log->save();
User::query()->where('user_id', $log->user_id)->update([
'vip_id' => null,
'start_at' => null,
'end_at' => null,
]);
} else {
// 未生效的,要生效
$log->status = VipStatus::ACTIVE;
$log->save();
User::query()->where('user_id', $log->user_id)->update([
'vip_id' => $log->vip_id,
'start_at' => $log->start_at,
'end_at' => $log->ent_at,
]);
}
}
});
}
}
-2
View File
@@ -12,8 +12,6 @@ class Kernel extends ConsoleKernel
*/
protected function schedule(Schedule $schedule): void
{
// 会员到期
$schedule->command('app:vip-expired')->hourly();
// URL状态检测
Schedule::command('app:chk-link')->daily();
}
-15
View File
@@ -1,15 +0,0 @@
<?php
namespace App\Enums;
enum CommissionType: int
{
// 申请提现
case Withdraw = 1;
// 邀请的代理消费提成
case Commission = 2;
// 推荐返佣
case Rebates = 3;
}
-11
View File
@@ -1,11 +0,0 @@
<?php
namespace App\Enums;
enum VipStatus: int
{
// 会员状态: 1未生效 2已生效 3已过期
case PENDING = 1;
case ACTIVE = 2;
case EXPIRED = 3;
}
-19
View File
@@ -11,9 +11,6 @@ class BaseConfig implements SimpleForm
public function policy(Request $request): array
{
return $request->validate([
'is_give_vip' => 'nullable', // 开启赠送
'give_vip_days' => 'nullable|integer|min:0|max:365', //赠送天数
'give_vip_id' => 'nullable|integer', //赠送套餐
'recommend_commission' => 'nullable|integer', // 推荐佣金
'web_site_customer_service' => 'nullable',
@@ -34,12 +31,6 @@ class BaseConfig implements SimpleForm
'mail_from_address' => 'nullable',
'mail_username' => 'nullable',
'mail_password' => 'nullable',
'wechat_pay_app_id' => 'nullable',
'wechat_pay_mch_id' => 'nullable',
'wechat_pay_secret_key' => 'nullable',
'wechat_pay_private_cert' => 'nullable',
'wechat_pay_certificate' => 'nullable',
]);
}
@@ -54,10 +45,6 @@ class BaseConfig implements SimpleForm
public function default(): array
{
return [
'is_give_vip' => (bool) SystemConfig::get('is_give_vip'),
'give_vip_id' => SystemConfig::get('give_vip_id'),
'give_vip_days' => SystemConfig::get('give_vip_days'),
'web_site_customer_service' => SystemConfig::get('web_site_customer_service'),
'web_site_title' => SystemConfig::get('web_site_title'),
'web_site_logo' => SystemConfig::get('web_site_logo'),
@@ -76,12 +63,6 @@ class BaseConfig implements SimpleForm
'mail_from_address' => SystemConfig::get('mail_from_address'),
'mail_username' => SystemConfig::get('mail_username'),
'mail_password' => SystemConfig::get('mail_password'),
'wechat_pay_app_id' => SystemConfig::get('wechat_pay_app_id'),
'wechat_pay_mch_id' => SystemConfig::get('wechat_pay_mch_id'),
'wechat_pay_secret_key' => SystemConfig::get('wechat_pay_secret_key'),
'wechat_pay_private_cert' => SystemConfig::get('wechat_pay_secret_key'), // 私钥证书
'wechat_pay_certificate' => SystemConfig::get('wechat_pay_certificate'), // 公钥证书
];
}
}
@@ -3,23 +3,16 @@
namespace App\Http\Controllers\Api;
use App\Enums\UserType;
use App\Enums\VipStatus;
use App\Http\Controllers\Controller;
use App\Http\Requests\ChangePasswordRequest;
use App\Http\Requests\LoginRequest;
use App\Http\Requests\RegisterRequest;
use App\Http\Requests\ResetPasswordRequest;
use App\Http\Resources\UserInfoResource;
use App\Jobs\PayVip;
use App\Models\Link;
use App\Models\User;
use App\Models\VipLogs;
use App\Models\VipPackage;
use App\PayChannels\WeChatPayNative;
use App\Services\SystemConfig;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Ugly\Base\Models\Payment;
use Ugly\Base\Traits\ApiResource;
class AuthController extends Controller
@@ -58,26 +51,7 @@ class AuthController extends Controller
$data['parent_id'] = $agent->id;
}
// 会员注册
$user = User::query()->create($data);
if (SystemConfig::get('is_give_vip')) {
// 赠送体验会员
$vip_days = (int) SystemConfig::get('give_vip_days');
$vip_id = (int) SystemConfig::get('give_vip_id');
if ($vip_days && $vip_id) {
$log = VipLogs::query()->create([
'user_id' => $user->id,
'vip_id' => $vip_id,
'status' => VipStatus::ACTIVE,
'start_at' => now(),
'end_at' => now()->addDays($vip_days),
]);
$user->vip_id = $vip_id;
$user->start_at = $log->start_at;
$user->end_at = $log->end_at;
$user->save();
}
}
User::query()->create($data);
return $this->success();
}
@@ -106,52 +80,15 @@ class AuthController extends Controller
{
$user = $request->user('api');
if ($user->type === UserType::MEMBER) {
$user->load('vipPackage:id,name,config');
$user->link_amount = $user->link_total = $user->link_created = 0;
$config = $user->vipPackage['config'] ?? null;
if ($config) {
$link_count = Link::query()->where('user_id', $user->id)->count();
$user->link_total = (int) $config['count_limit'];
$user->link_created = $link_count;
$user->link_amount = (int) bcsub($config['count_limit'], $link_count);
}
$link_count = Link::query()->where('user_id', $user->id)->count();
$link_expire = Link::query()->where('user_id', $user->id)->where('expired_at', '<', now())->count();
$user->link_created = $link_count;
$user->link_expire = $link_expire;
}
$user->login_time = $user->currentAccessToken()->created_at;
return $this->success(UserInfoResource::make($user));
}
// 购买会员
public function buyVip(Request $request): JsonResponse
{
$user = $request->user('api');
$vip = VipPackage::query()->findOrFail($request->input('vip_id'));
if (SystemConfig::get('wechat_pay_app_id') == null ||
SystemConfig::get('wechat_pay_mch_id') == null ||
SystemConfig::get('wechat_pay_secret_key') == null ||
SystemConfig::get('wechat_pay_secret_key') == null ||
SystemConfig::get('wechat_pay_certificate') == null) {
return $this->failed('清设置支付信息');
}
try {
$res = Payment::pay(
channel: WeChatPayNative::class,
amount: $vip->price,
job: PayVip::class,
attach: [
'vip_id' => $vip->id,
// 生效类型:1-立即生效 2-延期生效
'type' => $request->integer('type', 1),
],
payer: $user
)->send(['description' => '购买会员']);
return $this->success($res);
} catch (\Exception $e) {
info($e->getMessage());
return $this->failed('支付失败!');
}
}
}
@@ -4,22 +4,16 @@ namespace App\Http\Controllers\Api;
use App\Enums\UserType;
use App\Http\Controllers\Controller;
use App\Jobs\PayVip;
use App\Models\Domain;
use App\Models\Link;
use App\Models\LinkVisitLog;
use App\Models\MiniProgram;
use App\Models\Notice;
use App\Models\User;
use App\Models\VipLogs;
use App\Models\VipPackage;
use App\Services\SystemConfig;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
use Ugly\Base\Enums\PaymentStatus;
use Ugly\Base\Models\Payment;
use Ugly\Base\Traits\ApiResource;
class IndexController extends Controller
@@ -32,15 +26,31 @@ class IndexController extends Controller
$user = auth('api')->user();
// 超级管理的首页统计
if ($user->type === UserType::Admin) {
$res['user_count'] = (string) User::query()->where('type', UserType::MEMBER)->count();
$res['pay_count'] = User::query()->where('type', UserType::MEMBER)->whereNotNull('vip_id')->count();
$res['pay_vip'] = bcdiv(Payment::query()
->where('status', PaymentStatus::Success)
->where('job', PayVip::class)
->sum('amount'), 100, 2);
// 用户增长曲线 按月统计
$type = $request->integer('type', 1); // 1 最近30天;2 最近12个月
$res['user_count'] = User::query()
->where('type', UserType::MEMBER)
->where('created_at', '>=',
$type === 1 ?
now()->subDays(30)->startOfDay()
: now()->subYear()->startOfDay()
)
->count();
$res['card_count'] = Link::query()
->where('created_at', '>=',
$type === 1 ?
now()->subDays(30)->startOfDay()
: now()->subYear()->startOfDay()
)
->count();
$res['uv_count'] = LinkVisitLog::query()
->where('created_at', '>=',
$type === 1 ?
now()->subDays(30)->startOfDay()
: now()->subYear()->startOfDay()
)
->count();
$res['user_growth'] = User::query()
->select([
DB::raw('DATE_FORMAT(`created_at`, '.($type === 1 ? "'%Y-%m-%d'" : "'%Y-%m'").') AS date'),
@@ -62,39 +72,14 @@ class IndexController extends Controller
// 会员的统计
if ($user->type === UserType::MEMBER) {
$vip = VipPackage::query()->find($user->vip_id);
$res = [];
$res['link_count'] = Link::query()->where('user_id', $user->id)->count();
if ($vip) {
$res['used_uv'] = LinkVisitLog::query()
->where('user_id', $user->id)
->when($user->start_at, function ($query, $start_at) {
$query->where('created_at', '>=', $start_at);
})
->when($user->end_at, function ($query, $end_at) {
$query->where('created_at', '>=', $end_at);
})
->groupBy('device_uid')
->count();
}
$res['vip_logs'] = VipLogs::with(['payment', 'vipPackage'])
$res['used_uv'] = LinkVisitLog::query()
->where('user_id', $user->id)
->orderByDesc('id')
->limit(30)
->get()->transform(function ($item) {
return [
'id' => $item->id,
'amount' => $item->payment?->amount ?: 0,
'start_at' => $item->start_at,
'end_at' => $item->end_at,
'status' => $item->status,
'vip_package' => [
'id' => $item->vipPackage?->id,
'name' => $item->vipPackage?->name,
],
];
});
->count();
$res['month_uv'] = LinkVisitLog::query()
->where('user_id', $user->id)
->whereBetween('created_at', [now()->startOfMonth(), now()])
->count();
$res['notices'] = Notice::query()
->where('type', UserType::MEMBER)
@@ -114,7 +99,6 @@ class IndexController extends Controller
$configs = [
'code_mode' => SystemConfig::get('send_code_mode'),
'verify_code_is_open' => SystemConfig::get('verify_code_is_open'),
'package' => VipPackage::query()->orderBy('level')->get(['id', 'name', 'price', 'config']),
'web_site_customer_service' => SystemConfig::get('web_site_customer_service'),
'web_site_title' => SystemConfig::get('web_site_title'),
'web_site_logo' => SystemConfig::get('web_site_logo'),
@@ -130,7 +114,6 @@ class IndexController extends Controller
$configs['mini_programs'] = MiniProgram::query()
->where(
fn ($query) => $query->where('user_id', $user->id)
->when($user->getPackageConfig('pre_min'), fn ($query) => $query->orWhere('is_pre_min', true))
)
->get(['id', 'name']);
}
@@ -4,14 +4,12 @@ namespace App\Http\Controllers\Api;
use App\Enums\LinkType;
use App\Enums\SwitchType;
use App\Enums\UserType;
use App\Enums\UVLimitType;
use App\Http\Controllers\Controller;
use App\Models\Link;
use App\Models\LinkVisitLog;
use App\Models\MiniProgram;
use App\Models\User;
use App\Models\VipPackage;
use EasyWeChat\MiniApp\Application as WechatApp;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@@ -44,10 +42,6 @@ class JumpController extends Controller
if (empty($user->status)) {
return $this->failed('账号被禁用!');
}
$isAdmin = $user->type === UserType::Admin;
if (! $isAdmin && ! $user->vip_id) {
return $this->failed('会员已到期,禁止访问!');
}
$device_uid = $request->input('device_uid') ?: Str::uuid();
$cache = data_get(LinkVisitLog::query()
@@ -56,20 +50,6 @@ class JumpController extends Controller
->orderByDesc('id')
->first(), 'cache', []);
if (empty($cache)) {
if (! $isAdmin) {
// 会员UV限制
$totalUV = LinkVisitLog::query()
->where('link_id', $link->id)
->groupBy('device_uid')
->whereBetween('created_at', [$user->start_at, $user->end_at])
->count();
$limit = (int) data_get(VipPackage::query()->find($user->vip_id), 'config.uv_limit');
if ($limit > 0 && $totalUV >= $limit) {
return $this->failed('当前访问已达上限!');
}
}
$cache = [
'title' => $link->title,
'description' => $link->description,
@@ -64,36 +64,6 @@ class LinkController extends FormController
if ($user->type === UserType::Admin) {
return true;
}
if ($form->isCreate()) {
$config = data_get($user->getPackageConfig(), 'config');
// 类型限制
$typeName = LinkType::from((int) request()->input('type'))->name;
if (! data_get($config, 'allow_type.'.$typeName)) {
return '当前套餐不支持该类型链接!';
}
// 会员,数量限制
if (
$user->type === UserType::MEMBER &&
data_get($config, 'count_limit') <= Link::query()->where('user_id', $user->id)->count()
) {
return '拥有的链接数量已达上限!';
}
// 是否允许自定义落地页
$inputConfig = request()->input('config');
if (
! data_get($config, 'cur_index') &&
(
data_get($inputConfig, 'wx.avatar') ||
data_get($inputConfig, 'wx.title') ||
data_get($inputConfig, 'wx.sub_title')
)
) {
return '当前套餐不允许自定义落地页!';
}
}
return $form->isCreate() || $form->getModel()->user_id === $user->id;
});
@@ -16,22 +16,12 @@ class MiniProgramController extends FormController
public function home(): JsonResponse
{
$user = auth('api')->user();
$is_pre_min = false;
if ($user->start_at < now() && $user->end_at > now()) {
// 套餐包含官方小程序池使用
$is_pre_min = $user->getPackageConfig('pre_min');
}
$query = MiniProgram::search([
'name' => 'like',
'type' => '=',
'is_enable' => '=',
], ['user:id,username'])
->where(fn ($query) => $query->where('user_id', $user->id)
->when(
$is_pre_min,
fn ($query) => $query->orWhere('is_pre_min', true)
)
)
->where(fn ($query) => $query->where('user_id', $user->id))
->orderByDesc('id')
->get();
@@ -74,16 +64,8 @@ class MiniProgramController extends FormController
if ($user->type === UserType::Admin) {
return true;
}
if ($form->isCreate()) {
$min_count_limit = (int) $user->getPackageConfig('min_count_limit');
if ($min_count_limit <= MiniProgram::query()->where('user_id', $user->id)->count()) {
return '创建小程序数量已达上限!';
}
return true;
}
return $form->getModel()->user_id === $user->id;
return $form->isCreate() || $form->getModel()->user_id === $user->id;
});
$form->handleInput(function (FormService $formService) use ($user) {
@@ -1,28 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Resources\PaymentResource;
use App\Models\VipPackage;
use Ugly\Base\Models\Payment;
use Ugly\Base\Traits\ApiResource;
class PaymentController extends Controller
{
use ApiResource;
public function index()
{
$query = Payment::search([
'status' => '=',
'payer.username' => 'like',
'created_at' => 'between',
'success_at' => 'between',
], ['payer:id,username,type'])->orderByDesc('id');
return $this->paginate($query, PaymentResource::class, [
'vip' => VipPackage::query()->pluck('name', 'id'),
]);
}
}
@@ -6,7 +6,6 @@ use App\Enums\UserType;
use App\Http\Controllers\Controller;
use App\Http\Resources\UserResource;
use App\Models\User;
use App\Models\VipPackage;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Ugly\Base\Traits\ApiResource;
@@ -20,7 +19,7 @@ class UserController extends Controller
$query = User::search([
'type' => '=',
'username' => 'like',
], ['vipPackage:id,name', 'parent'])
], ['parent'])
->whereIn('type', [UserType::MEMBER])
->orderByDesc('id');
@@ -57,23 +56,6 @@ class UserController extends Controller
$params = $request->only(['status', 'vip_id', 'end_at', 'credit']);
$pack_id = $params['vip_id'] ?? null;
$model = VipPackage::class;
$pak = $user->getPackageConfig();
if ($pak?->id == $pack_id) {
return $this->failed('没有修改');
}
if ($pack_id) {
if (app($model)->where('id', $pack_id)->doesntExist()) {
return $this->failed('套餐不存在');
}
$params['end_at'] = now()->addMonth();
if (empty($user->start_at)) {
$user->start_at = now();
}
}
$user->update($params);
return $this->success();
@@ -1,48 +0,0 @@
<?php
namespace App\Http\Controllers\Api;
use App\Enums\LinkType;
use App\Models\User;
use App\Models\VipPackage;
use Illuminate\Http\JsonResponse;
use Ugly\Base\Exceptions\ApiCustomError;
use Ugly\Base\Http\Controllers\FormController;
use Ugly\Base\Services\FormService;
class VipPackageController extends FormController
{
public function index(): JsonResponse
{
return $this->success(VipPackage::query()->get());
}
protected function form(): FormService
{
$form = FormService::make(VipPackage::class);
$form->validate(fn (FormService $form) => [
'name' => ['required', $form->unique()],
'price' => 'required|numeric|min:0',
'level' => 'required|integer|min:0',
'config' => 'required|array',
'config.uv_limit' => 'required|integer|min:0',
'config.count_limit' => 'required|integer|min:0', // 链接数量限制
'config.min_count_limit' => 'required|integer|min:0',
'config.pre_min' => 'required|boolean', // 平台落地小程序
'config.min_disabled_check' => 'required|boolean',
'config.support' => 'required|boolean',
'config.cur_index' => 'required|boolean',
'config.allow_type' => 'required_array_keys:'.implode(',', LinkType::getAllType()),
'config.allow_type.*' => 'required|boolean',
]);
$form->deleting(function (FormService $form) {
throw_if(
User::query()->where('vip_id', $form->getKey())->exists(),
new ApiCustomError('该套餐下有未过期的会员,不能删除!')
);
});
return $form;
}
}
@@ -2,7 +2,7 @@
namespace App\Http\Controllers;
class JumpController extends Controller
class HomeController extends Controller
{
public function show(string $code): \Illuminate\View\View
{
@@ -2,7 +2,6 @@
namespace App\Http\Resources;
use App\Enums\UserType;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
@@ -22,10 +21,6 @@ class UserResource extends JsonResource
'type' => $this->type,
'start_at' => $this->start_at,
'end_at' => $this->end_at,
// 'credit' => $this->credit,
$this->mergeWhen($this->type === UserType::MEMBER, [
'vip_package' => $this->vipPackage,
]),
'parent' => new UserSimpleResource($this->whenLoaded('parent')),
'created_at' => $this->created_at?->format('Y-m-d H:i:s'),
'login_time' => $this->login_time?->format('Y-m-d H:i:s'),
-98
View File
@@ -1,98 +0,0 @@
<?php
namespace App\Jobs;
use App\Enums\CommissionType;
use App\Enums\VipStatus;
use App\Enums\WithdrawStatus;
use App\Models\VipLogs;
use App\Services\DistributorsService;
use App\Services\SystemConfig;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Carbon;
class PayVip implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*/
public function __construct(protected $payment)
{
//
}
/**
* Execute the job.
*/
public function handle(): void
{
$attach = data_get($this->payment, 'attach', []);
$user = $this->payment->payer;
$type = data_get($attach, 'type', 1); // 1-立即生效 2-延期生效
if ($type === 1) {
// 1-立即生效
$start_at = now();
$status = VipStatus::ACTIVE;
} else {
// 2-延期生效
$info = VipLogs::query()
->where('user_id', $user->id)
->where('status', [VipStatus::PENDING, VipStatus::ACTIVE])
->orderByDesc('end_at')
->first();
$start_at = $info ? Carbon::parse($info->end_at) : now();
$status = VipStatus::PENDING;
}
// 创建记录
VipLogs::query()->create([
'user_id' => $user->id,
'payment_id' => $this->payment->id,
'vip_id' => $attach['vip_id'],
'status' => $status,
'start_at' => $start_at,
'end_at' => $start_at->addMonth(),
]);
if ($status === VipStatus::ACTIVE) {
$user->vip_id = $attach['vip_id'];
$user->start_at = now();
$user->end_at = now()->addMonth();
$user->save();
}
// 一级分销
$this->distributors(
(int) SystemConfig::get('member_pay_commission_1'),
$user,
);
// 二级分销
$this->distributors(
(int) SystemConfig::get('member_pay_commission_2'),
$user->parent,
'二级分销邀请人消费提成',
);
}
private function distributors($rate, $user, $title = null): void
{
if (! empty($rate)) {
$commission = bcdiv(bcmul($this->payment->amount, $rate, 4), 100, 4);
DistributorsService::commission(
$commission,
$user,
[
'title' => $title ?? '邀请人消费提成',
'type' => CommissionType::Commission,
'is_withdraw' => false,
'status' => WithdrawStatus::NONE,
]
);
}
}
}
-23
View File
@@ -35,12 +35,6 @@ class User extends Authenticatable
});
}
// vip套餐
public function vipPackage(): BelongsTo
{
return $this->belongsTo(VipPackage::class, 'vip_id');
}
// 推荐人
public function parent(): BelongsTo
{
@@ -52,21 +46,4 @@ class User extends Authenticatable
{
return $this->hasMany(self::class, 'parent_id');
}
// 佣金记录
public function commissionLogs(): HasMany
{
return $this->hasMany(CommissionLog::class, 'user_id');
}
// 获取权益配置
public function getPackageConfig(?string $key = null)
{
$package = null;
if ($this->vip_id) {
$package = $this->vipPackage;
}
return $key ? data_get($package, 'config.'.$key) : $package;
}
}
-29
View File
@@ -1,29 +0,0 @@
<?php
namespace App\Models;
use App\Enums\VipStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Ugly\Base\Models\Payment;
class VipLogs extends Model
{
protected $guarded = [];
protected $casts = [
'status' => VipStatus::class,
];
// 对应的payment
public function payment(): BelongsTo
{
return $this->belongsTo(Payment::class);
}
// 对应的会员套餐
public function vipPackage(): BelongsTo
{
return $this->belongsTo(VipPackage::class, 'vip_id');
}
}
-19
View File
@@ -1,19 +0,0 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Ugly\Base\Casts\Amount;
use Ugly\Base\Traits\SerializeDate;
class VipPackage extends Model
{
use SerializeDate;
protected $guarded = [];
protected $casts = [
'price' => Amount::class,
'config' => 'json',
];
}
-95
View File
@@ -1,95 +0,0 @@
<?php
namespace App\PayChannels;
use App\Services\SystemConfig;
use EasyWeChat\Pay\Application;
use Illuminate\Support\Carbon;
use Symfony\Component\HttpFoundation\Response;
use Ugly\Base\Exceptions\ApiCustomError;
use Ugly\Base\Models\Payment;
use Ugly\Base\Traits\ApiResource;
class WeChatPayNative
{
use ApiResource;
private Application $payApp;
public function __construct()
{
$config = [
'appid' => SystemConfig::get('wechat_pay_app_id'),
'mch_id' => SystemConfig::get('wechat_pay_mch_id'),
'secret_key' => SystemConfig::get('wechat_pay_secret_key'),
// 商户证书
'private_key' => SystemConfig::get('wechat_pay_secret_key'),
'certificate' => SystemConfig::get('wechat_pay_certificate'),
/*'platform_certs' => [
storage_path('/certs/wechatpay.pem'),
],*/
'http' => [
'throw' => false,
],
];
$this->payApp = new Application($config);
}
// 支付
public function pay($payment, array $data = []): array
{
$payInfo = $this->payApp->getClient()->postJson('v3/pay/transactions/native', [
'appid' => config('services.wechat_pay.appid'),
'mchid' => config('services.wechat_pay.mch_id'),
'description' => data_get($data, 'description', ''),
'out_trade_no' => $payment->no,
'notify_url' => config('app.url').'/api/wechat/payment_notify',
'time_expire' => $payment->expired_at->toRfc3339String(),
'amount' => [
'total' => (int) bcmul($payment->amount, 100, 0),
'currency' => 'CNY',
],
])->toArray();
throw_if(
! isset($payInfo['code_url']),
new ApiCustomError(data_get($payInfo, 'message', '微信下单失败!'), Response::HTTP_INTERNAL_SERVER_ERROR, Response::HTTP_INTERNAL_SERVER_ERROR)
);
return [
'no' => $payment->no,
'amount' => $payment->amount,
'desc' => $data['description'] ?? '',
'expired_at' => $payment->expired_at->toDateTimeString(),
'code_url' => $payInfo['code_url'],
];
}
// 成功后通知.
public function notify()
{
$server = $this->payApp->getServer();
// 支付成功
$server->handlePaid(function ($message) {
Payment::success($message['out_trade_no'], [
'notification_no' => $message['transaction_id'],
'notification_data' => $message,
'success_at' => Carbon::parse($message['success_time']),
]);
});
// 退款成功
$server->handleRefunded(function ($message) {
if ($message['refund_status'] === 'SUCCESS') {
Payment::success($message['out_trade_no'], [
'notification_no' => $message['refund_id'],
'notification_data' => $message,
'success_at' => Carbon::parse($message['success_time']),
]);
}
});
return $server->serve();
}
}
-15
View File
@@ -31,21 +31,6 @@ return [
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
],
// 微信支付
'wechat_pay' => [
'appid' => env('WECHAT_PAY_APP_ID'),
'mch_id' => env('WECHAT_PAY_MCH_ID'),
'secret_key' => env('WECHAT_PAY_SECRET_KEY'),
// 商户证书
'private_key' => storage_path('/certs/apiclient_key.pem'),
'certificate' => storage_path('/certs/apiclient_cert.pem'),
'platform_certs' => [
storage_path('/certs/wechatpay.pem'),
],
'http' => [
'throw' => false,
],
],
// 阿里短信配置
'ali_sms' => [
'key' => env('ALI_SMS_KEY'),
-99
View File
@@ -253,16 +253,13 @@ INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (1, '0001_01_01_000
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (2, '0001_01_01_000001_create_cache_table', 1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (3, '0001_01_01_000002_create_jobs_table', 1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (4, '2019_12_14_000001_create_personal_access_tokens_table', 1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (5, '2024_04_10_095245_create_vip_packages_table', 1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (6, '2024_04_10_101023_create_notices_table', 1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (7, '2024_04_10_101140_create_domains_table', 1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (8, '2024_04_10_101217_create_mini_programs_table', 1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (9, '2024_04_10_101507_create_links_table', 1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (10, '2024_04_10_103926_create_link_visit_logs_table', 1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (11, '2024_04_10_104736_create_payments_table', 1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (12, '2024_04_10_104736_create_sys_configs_table', 1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (13, '2024_04_15_140100_create_materials_table', 1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (14, '2024_04_16_084221_create_vip_logs_table', 1);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (15, '2024_05_08_101122_create_material_categories_table', 1);
COMMIT;
@@ -313,47 +310,6 @@ CREATE TABLE `notices` (
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for payments
-- ----------------------------
DROP TABLE IF EXISTS `payments`;
CREATE TABLE `payments` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`no` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '支付编号',
`order_no` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '内部订单号',
`channel` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '支付渠道',
`amount` bigint unsigned NOT NULL COMMENT '支付金额/分',
`type` tinyint unsigned NOT NULL COMMENT '支付类型: 1付款 2退款 3转账',
`status` tinyint unsigned NOT NULL COMMENT '状态: 1处理中 2成功 3失败',
`success_at` timestamp NULL DEFAULT NULL COMMENT '成功时间',
`fail_at` timestamp NULL DEFAULT NULL COMMENT '失败时间',
`expired_at` timestamp NULL DEFAULT NULL COMMENT '过期时间',
`remark` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`notification_no` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '第三方支付通知单号',
`notification_data` json DEFAULT NULL COMMENT '第三方支付通知原始数据',
`job` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '回调任务',
`attach` json DEFAULT NULL COMMENT '附加信息',
`payment_id` bigint unsigned DEFAULT NULL COMMENT '退款时关联支付单ID',
`merchant_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`merchant_id` bigint unsigned DEFAULT NULL,
`payer_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`payer_id` bigint unsigned DEFAULT NULL,
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `payments_no_unique` (`no`),
KEY `payments_merchant_type_merchant_id_index` (`merchant_type`,`merchant_id`),
KEY `payments_payer_type_payer_id_index` (`payer_type`,`payer_id`),
KEY `payments_type_index` (`type`),
KEY `payments_status_index` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支付记录表';
-- ----------------------------
-- Records of payments
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for personal_access_tokens
-- ----------------------------
@@ -400,9 +356,6 @@ BEGIN;
INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`) VALUES ('ali_sms_key', '', '阿里短信key', NULL, NULL);
INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`) VALUES ('ali_sms_secret', '', '阿里短信secret', NULL, NULL);
INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`) VALUES ('ali_sms_sign_name', '', '阿里短信签名', NULL, NULL);
INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`) VALUES ('give_vip_days', '3', '注册赠送会员有效期/天', NULL, NULL);
INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`) VALUES ('give_vip_id', '1', '赠送套餐', NULL, NULL);
INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`) VALUES ('is_give_vip', '1', '开启注册赠送会员', NULL, NULL);
INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`) VALUES ('mail_from_address', '', '发信地址', NULL, NULL);
INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`) VALUES ('mail_from_name', '', '发信名称', NULL, NULL);
INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`) VALUES ('mail_host', '', '服务器地址', NULL, NULL);
@@ -415,11 +368,6 @@ INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`)
INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`) VALUES ('web_site_customer_service', '/image/web-qr.png', '客服二维码', NULL, NULL);
INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`) VALUES ('web_site_logo', '/image/toplogo.png', '网站logo浅色', NULL, NULL);
INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`) VALUES ('web_site_title', '蚂蚁快链-智慧引流工具', '网站名称', NULL, NULL);
INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`) VALUES ('wechat_pay_app_id', '', '微信商户appId', NULL, NULL);
INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`) VALUES ('wechat_pay_certificate', '', '微信支付公钥证书', NULL, NULL);
INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`) VALUES ('wechat_pay_mch_id', '', '微信商户号', NULL, NULL);
INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`) VALUES ('wechat_pay_private_cert', '', '微信支付私钥证书', NULL, NULL);
INSERT INTO `sys_configs` (`slug`, `value`, `desc`, `created_at`, `updated_at`) VALUES ('wechat_pay_secret_key', '', '微信支付密钥', NULL, NULL);
COMMIT;
-- ----------------------------
@@ -457,51 +405,4 @@ BEGIN;
INSERT INTO `users` (`id`, `username`, `password`, `status`, `type`, `credit`, `accumulate_credit`, `commission`, `accumulate_commission`, `vip_id`, `agent_id`, `level_id`, `start_at`, `end_at`, `parent_id`, `referral_code`, `created_at`, `updated_at`) VALUES (1, 'admin', '$2y$12$DQNA/1BJcPdmpJ9ylifXHO9X.RDaovOj4SHL5M6S/vr9lvnFND5Gy', 1, 3, 0, 0, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL, '100000', '2025-01-15 08:41:32', '2025-01-15 08:41:32');
COMMIT;
-- ----------------------------
-- Table structure for vip_logs
-- ----------------------------
DROP TABLE IF EXISTS `vip_logs`;
CREATE TABLE `vip_logs` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`user_id` bigint unsigned NOT NULL COMMENT '会员id',
`payment_id` bigint unsigned DEFAULT NULL COMMENT '支付ID',
`vip_id` bigint unsigned DEFAULT NULL COMMENT '会员ID',
`status` tinyint unsigned NOT NULL COMMENT '状态',
`start_at` timestamp NOT NULL COMMENT '开始时间',
`end_at` timestamp NOT NULL COMMENT '结束时间',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='VIP购买记录表';
-- ----------------------------
-- Records of vip_logs
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for vip_packages
-- ----------------------------
DROP TABLE IF EXISTS `vip_packages`;
CREATE TABLE `vip_packages` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '套餐名称',
`price` bigint unsigned NOT NULL COMMENT '价格/月',
`level` tinyint unsigned NOT NULL COMMENT '等级',
`config` json NOT NULL COMMENT '权益配置',
`created_at` timestamp NULL DEFAULT NULL,
`updated_at` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='vip套餐配置';
-- ----------------------------
-- Records of vip_packages
-- ----------------------------
BEGIN;
INSERT INTO `vip_packages` (`id`, `name`, `price`, `level`, `config`, `created_at`, `updated_at`) VALUES (1, '体验套餐', 0, 0, '{\"pre_min\": false, \"support\": true, \"uv_limit\": 9, \"cur_index\": false, \"allow_type\": {\"CLI_QR\": false, \"KING_DOC\": false, \"WORK_WECHAT\": false, \"LANDING_MINI\": false, \"MINI_PROGRAM\": true}, \"count_limit\": 1, \"min_count_limit\": 1, \"min_disabled_check\": true}', '2024-04-11 12:49:52', '2024-09-29 08:28:55');
INSERT INTO `vip_packages` (`id`, `name`, `price`, `level`, `config`, `created_at`, `updated_at`) VALUES (2, '初级会员', 4300, 1, '{\"pre_min\": true, \"support\": true, \"uv_limit\": 50, \"cur_index\": true, \"allow_type\": {\"CLI_QR\": true, \"KING_DOC\": true, \"WORK_WECHAT\": true, \"LANDING_MINI\": true, \"MINI_PROGRAM\": true}, \"count_limit\": 5, \"min_count_limit\": 5, \"min_disabled_check\": true}', '2024-04-17 14:27:19', '2024-09-29 08:29:32');
INSERT INTO `vip_packages` (`id`, `name`, `price`, `level`, `config`, `created_at`, `updated_at`) VALUES (3, '高级会员', 29800, 2, '{\"pre_min\": true, \"support\": true, \"uv_limit\": 100000, \"cur_index\": true, \"allow_type\": {\"CLI_QR\": true, \"KING_DOC\": true, \"WORK_WECHAT\": true, \"LANDING_MINI\": true, \"MINI_PROGRAM\": true}, \"count_limit\": 50, \"min_count_limit\": 10, \"min_disabled_check\": true}', '2024-04-22 17:40:28', '2024-09-29 08:29:49');
COMMIT;
SET FOREIGN_KEY_CHECKS = 1;
@@ -1,32 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('vip_packages', function (Blueprint $table) {
$table->comment('vip套餐配置');
$table->id();
$table->string('name')->comment('套餐名称');
$table->unsignedBigInteger('price')->comment('价格/月');
$table->unsignedTinyInteger('level')->comment('等级');
$table->json('config')->comment('权益配置');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('vip_packages');
}
};
@@ -1,34 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('vip_logs', function (Blueprint $table) {
$table->comment('VIP购买记录表');
$table->id();
$table->unsignedBigInteger('user_id')->comment('会员id');
$table->unsignedBigInteger('payment_id')->nullable()->comment('支付ID');
$table->unsignedBigInteger('vip_id')->nullable()->comment('会员ID');
$table->unsignedTinyInteger('status')->comment('状态');
$table->timestamp('start_at')->comment('开始时间');
$table->timestamp('end_at')->comment('结束时间');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('vip_logs');
}
};
-13
View File
@@ -11,13 +11,10 @@ use App\Http\Controllers\Api\MaterialCateController;
use App\Http\Controllers\Api\MaterialController;
use App\Http\Controllers\Api\MiniProgramController;
use App\Http\Controllers\Api\NoticeController;
use App\Http\Controllers\Api\PaymentController;
use App\Http\Controllers\Api\UploadController;
use App\Http\Controllers\Api\UserController;
use App\Http\Controllers\Api\VipPackageController;
use App\Http\Controllers\UpgradeController;
use App\Http\Middleware\ApiAuth;
use App\PayChannels\WeChatPayNative;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Route;
@@ -27,7 +24,6 @@ Route::post('/register', [AuthController::class, 'register']); // 注册
Route::post('/reset-password', [AuthController::class, 'resetPassword']); // 重置密码
Route::get('/captcha/image', [CaptchaController::class, 'image']); // 登录
Route::post('/captcha/sms', [CaptchaController::class, 'sms']); // 获取短信验证码
Route::post('/wechat/payment_notify', [WeChatPayNative::class, 'notify']); // 微信支付回调
// 获取链接重定向目标
Route::get('/link-target/{code}', [JumpController::class, 'target']); // 获取链接跳转地址
@@ -58,9 +54,6 @@ Route::middleware(ApiAuth::class)->group(function (Router $router) {
$router->get('link-list', [LinkController::class, 'link_list']); // 获取下拉数据源
$router->get('/agent-invite', [UserController::class, 'invite']); // 邀请记录
// 购买会员
$router->post('/buy-vip', [AuthController::class, 'buyVip']);
});
// 仅管理员可访问
@@ -69,12 +62,8 @@ Route::middleware(ApiAuth::class.':admin')->group(function (Router $router) {
$router->apiResource('notices', NoticeController::class)->except('show');
// 添加/编辑公告
$router->put('notice', [NoticeController::class, 'set_notice']);
// 域名
$router->apiResource('domains', DomainController::class)->except(['index', 'show']);
// VIP套餐
$router->apiResource('vip-packages', VipPackageController::class)->except('show');
// 设置
$router->get('config/{type}', [ConfigController::class, 'getForm']);
// 保存设置
@@ -82,7 +71,5 @@ Route::middleware(ApiAuth::class.':admin')->group(function (Router $router) {
// 用户
$router->apiResource('users', UserController::class)->only(['index', 'store', 'update']);
$router->get('agent-tree', [UserController::class, 'agent_tree']); // 树状用户
$router->get('payments', [PaymentController::class, 'index']); // 付款记录
});
Route::get('upgrade_status', [UpgradeController::class, 'index']); // 请求最新版本
+2 -2
View File
@@ -1,11 +1,11 @@
<?php
use App\Http\Controllers\Controller;
use App\Http\Controllers\HomeController;
use App\Http\Controllers\Install\IndexController;
use App\Http\Controllers\JumpController;
use Illuminate\Support\Facades\Route;
Route::get('/', [Controller::class, 'welcome']);
Route::get('/j/{code}', [JumpController::class, 'show']);
Route::get('/j/{code}', [HomeController::class, 'show']);
Route::any('/install', [IndexController::class, 'index']);
+1 -43
View File
@@ -2,17 +2,15 @@
namespace Tests\Feature;
use App\Enums\CommissionType;
use App\Jobs\SendEmailJobs;
use App\Models\User;
use App\Models\VipPackage;
use Tests\TestCase;
class UserTest extends TestCase
{
public function test_send_mail()
{
SendEmailJobs::dispatch('724323954@qq.com', '123321');
SendEmailJobs::dispatch('123321@qq.com', '123321');
}
public function test_get_token()
@@ -39,14 +37,6 @@ class UserTest extends TestCase
$this->assertDatabaseHas('users', [
'username' => $phone,
]);
if (! empty($referral_code)) {
$child = User::query()->where('parent_id', $agent->id)->orderByDesc('id')->first();
$this->assertDatabaseHas('commission_logs', [
'user_id' => $agent->id,
'type' => CommissionType::Rebates->value,
'children_user_id' => $child->id,
]);
}
}
private function get_admin_token()
@@ -55,36 +45,4 @@ class UserTest extends TestCase
return $admin->createToken('api')->plainTextToken;
}
public function test_user_package_fix(): void
{
$token = $this->get_admin_token();
$user = User::query()
->inRandomOrder()
->where('id', 2)
->first();
$id = $user->id;
$u_vip_id = $user->vip_id;
$vip = VipPackage::query()
->when(! empty($u_vip_id), function ($query) use ($u_vip_id) {
$query->whereNot('id', $u_vip_id);
})
->inRandomOrder()
->first();
$response = $this
->withHeaders([
'Authorization' => "Bearer {$token}",
])
->putJson("/api/users/{$id}", [
'vip_id' => $vip->id,
]);
$response->assertStatus(200);
$this->assertDatabaseHas('users', [
'vip_id' => $vip->id,
// 'end_at' => '', // TODO
]);
}
}