Compare commits

..
Author SHA1 Message Date
dependabot[bot] 9d431b20a4 Bump rollup from 3.26.3 to 3.29.5 in /admin
Bumps [rollup](https://github.com/rollup/rollup) from 3.26.3 to 3.29.5.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v3.26.3...v3.29.5)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-01-02 02:04:57 +00:00
Marmot a1de189afc 改版 新增腾讯优码 2024-12-31 17:20:07 +08:00
atshike 6395633719 Merge pull request #4 from surprise-tech/dev
Dev
2024-11-07 09:56:01 +08:00
finalz c91989e6ed Update README.md 2024-11-05 12:24:23 +08:00
finalz ee12fbcea3 Update README.md 2024-11-05 11:44:26 +08:00
finalz f95ac4a6d8 Update README.md 2024-11-05 11:36:52 +08:00
finalz 0eaf087177 Update README.md 2024-11-04 13:43:26 +08:00
finalz c8437b28af Update README.md 2024-11-04 13:42:23 +08:00
41 changed files with 1295 additions and 672 deletions
+24 -2
View File
@@ -6,6 +6,20 @@
> 作为一款私域引流工具,凭借其强大的功能和灵活的应用场景,成为了推广营销领域的热门选择,它支持从抖音直接跳转到个人微信、微信群、小程序以及企业微信客服,有效打通了不同平台之间的壁垒,实现了流量的高效转化和利用,推广营销必备!
![image](https://github.com/user-attachments/assets/76f93840-4453-4aae-ac50-7b01a4010152)
### 系统演示
```
演示地址:https://open.mayilink.cn/
管理员账号:admin
管理员密码:admin123
用户账号:13999999999
用户密码:123456
```
### 目录结构
```
- admin 后台管理
@@ -76,7 +90,15 @@ let baseUrl = "https://xxx.xxxxx.xxx"; // 后台请求域名改为你的域名
## 重要信息
欢迎进群获取最新开发计划或交流技术
如需进群请添加微信号
微信号:tok4mi
添加请备注:交流群
项目合作洽谈,请联系客服微信(使用微信扫码添加好友,请注明来意)。
![](https://huoxing.tos-cn-shanghai.volces.com/2024/09/28/pehhLja89Gb0TIRWDkghVZ2mxwLd7uGiUq3NwK4P.png "微信")
<img src="https://huoxing.tos-cn-shanghai.volces.com/2024/11/05/HFiFy0AaqSLxmlFb0RScBKgLtGVzC9xXb7M9HCJ5.jpg" width="400">
+1
View File
@@ -29,6 +29,7 @@
"qs": "^6.11.2",
"sortablejs": "^1.15.0",
"vue": "^3.3.4",
"vue-cropper": "^1.1.4",
"vue-router": "^4.2.2"
},
"devDependencies": {
+19 -4
View File
@@ -59,6 +59,9 @@ importers:
vue:
specifier: ^3.3.4
version: 3.3.4
vue-cropper:
specifier: ^1.1.4
version: 1.1.4
vue-router:
specifier: ^4.2.2
version: 4.2.2(vue@3.3.4)
@@ -322,6 +325,7 @@ packages:
'@humanwhocodes/config-array@0.11.10':
resolution: {integrity: sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==}
engines: {node: '>=10.10.0'}
deprecated: Use @eslint/config-array instead
'@humanwhocodes/module-importer@1.0.1':
resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
@@ -329,6 +333,7 @@ packages:
'@humanwhocodes/object-schema@1.2.1':
resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==}
deprecated: Use @eslint/object-schema instead
'@jridgewell/sourcemap-codec@1.4.15':
resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==}
@@ -495,6 +500,7 @@ packages:
'@volar/vue-typescript@1.6.5':
resolution: {integrity: sha512-er9rVClS4PHztMUmtPMDTl+7c7JyrxweKSAEe/o/Noeq2bQx6v3/jZHVHBe8ZNUti5ubJL/+Tg8L3bzmlalV8A==}
deprecated: 'WARNING: This project has been renamed to @vue/typescript. Install using @vue/typescript instead.'
peerDependencies:
typescript: '*'
@@ -1134,6 +1140,7 @@ packages:
eslint@8.39.0:
resolution: {integrity: sha512-mwiok6cy7KTW7rBpo05k6+p4YVZByLNjAZ/ACB9DRCu4YDRwjXI01tWHp6KAUWelsBetTxKK/2sHB0vdS8Z2Og==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options.
hasBin: true
espree@9.6.1:
@@ -1313,6 +1320,7 @@ packages:
glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
deprecated: Glob versions prior to v9 are no longer supported
globals@13.20.0:
resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==}
@@ -1448,6 +1456,7 @@ packages:
inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
inherits@2.0.4:
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
@@ -2109,10 +2118,11 @@ packages:
rimraf@3.0.2:
resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==}
deprecated: Rimraf versions prior to v4 are no longer supported
hasBin: true
rollup@3.26.3:
resolution: {integrity: sha512-7Tin0C8l86TkpcMtXvQu6saWH93nhG3dGQ1/+l5V2TDMceTxO7kDiK6GzbfLWNNxqJXm591PcEZUozZm51ogwQ==}
rollup@3.29.5:
resolution: {integrity: sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==}
engines: {node: '>=14.18.0', npm: '>=8.0.0'}
hasBin: true
@@ -2507,6 +2517,9 @@ packages:
terser:
optional: true
vue-cropper@1.1.4:
resolution: {integrity: sha512-5m98vBsCEI9rbS4JxELxXidtAui3qNyTHLHg67Qbn7g8cg+E6LcnC+hh3SM/p94x6mFh6KRxT1ttnta+wCYqWA==}
vue-demi@0.14.5:
resolution: {integrity: sha512-o9NUVpl/YlsGJ7t+xuqJKx8EBGf1quRhCiT6D/J0pfwmk9zUwYkC7yrF4SZCe6fETvSM3UNL2edcbYrSyc4QHA==}
engines: {node: '>=12'}
@@ -4811,7 +4824,7 @@ snapshots:
dependencies:
glob: 7.2.3
rollup@3.26.3:
rollup@3.29.5:
optionalDependencies:
fsevents: 2.3.2
@@ -5275,12 +5288,14 @@ snapshots:
dependencies:
esbuild: 0.17.19
postcss: 8.4.26
rollup: 3.26.3
rollup: 3.29.5
optionalDependencies:
'@types/node': 18.16.17
fsevents: 2.3.2
sass: 1.64.0
vue-cropper@1.1.4: {}
vue-demi@0.14.5(vue@3.3.4):
dependencies:
vue: 3.3.4
+3
View File
@@ -0,0 +1,3 @@
{
"VITE_API_URL": "https://xxxx.xxxxxx.com/api"
}
+3
View File
@@ -48,3 +48,6 @@ export const ApiConsumeCard = (secret: string) => {
export const ApiUserFix = (id: any, data: any) => {
return http.put(`/users/${id}`, data)
}
export const ApiUploadBase64 = (data: any) => {
return http.post<any, any>('/material-base64', data)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 784 KiB

After

Width:  |  Height:  |  Size: 776 KiB

+224
View File
@@ -0,0 +1,224 @@
<template>
<el-button type="primary" @click="dialogVisible = true">上传图片</el-button>
<el-dialog
title="上传图片"
v-model="dialogVisible"
:show-close="false"
:close-on-click-modal="false"
:close-on-press-escape="false"
destroy-on-close
width="600px"
>
<div class="avatar-container">
<!-- 待上传图片 -->
<div v-show="!options.img">
<el-upload
class="upload"
ref="tailorUpload"
action="#"
:on-change="upload"
:show-file-list="false"
:auto-upload="false"
>
<el-button slot="trigger" type="primary" ref="uploadBtn"> 选择图片 </el-button>
</el-upload>
<div>支持jpgpng格式的图片大小不超过5M</div>
</div>
<div v-show="options.img" class="avatar-crop">
<vueCropper
class="crop-box"
ref="cropper"
:img="options.img"
:autoCrop="options.autoCrop"
:fixedBox="options.fixedBox"
:canMoveBox="options.canMoveBox"
:autoCropWidth="options.autoCropWidth"
:autoCropHeight="options.autoCropHeight"
:centerBox="options.centerBox"
:fixed="options.fixed"
:fixedNumber="options.fixedNumber"
:canMove="options.canMove"
:canScale="options.canScale"
:full="options.full"
:outputSize="0.1"
outputType="png"
></vueCropper>
</div>
</div>
<template #footer>
<div class="dialog-footer">
<div class="reupload" @click="reupload">
<span v-show="options.img">重新上传</span>
</div>
<div>
<el-button @click="closeDialog" :loading="loading"> </el-button>
<el-button type="primary" @click="getCrop" :loading="loading"> </el-button>
</div>
</div>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { VueCropper } from 'vue-cropper'
import { ref, reactive } from 'vue'
import 'vue-cropper/dist/index.css'
import { ElMessage, type UploadProps } from 'element-plus'
import { ApiUploadBase64 } from '@/api/user'
const cropper = ref()
const uploadBtn = ref()
const tailorUpload = ref()
const dialogVisible = ref(false)
const loading = ref(false)
const options = reactive({
img: '', // 原图文件
autoCrop: true, // 默认生成截图框
fixedBox: false, // 固定截图框大小
canMoveBox: true, // 截图框可以拖动
autoCropWidth: 200, // 截图框宽度
autoCropHeight: 200, // 截图框高度
fixed: true, // 截图框宽高固定比例
fixedNumber: [1, 1], // 截图框的宽高比例
centerBox: true, // 截图框被限制在图片里面
canMove: true, // 上传图片不允许拖动
canScale: true, // 上传图片不允许滚轮缩放
full: false //是否输出原图比例的截图
} as any)
const emits = defineEmits(['update:modelValue'])
const props = defineProps<{
modelValue?: string | string[]
}>()
const upload: UploadProps['onChange'] = (file: any) => {
const isIMAGE = file.raw.type === 'image/jpeg' || file.raw.type === 'image/png'
const isLt5M = file.raw.size / 1024 / 1024 < 5
if (!isIMAGE) {
ElMessage.warning('请选择 jpg、png 格式的图片')
return false
}
if (!isLt5M) {
ElMessage.warning('图片大小不能超过 5MB')
return false
}
let reader = new FileReader()
reader.readAsDataURL(file.raw)
//console.log(reader);
reader.onload = (e: any) => {
options.img = e.target.result // base64
}
tailorUpload.value.clearFiles() //这里处理重新上传时,upload组件change事件错误问题
}
// 关闭弹框
const closeDialog = () => {
options.img = ''
dialogVisible.value = false
}
const dealImage = (base64: any, w: any, callback: any) => {
const newImage: any = new Image()
let quality = 0.8 //压缩系数0-1之间
newImage.src = base64
newImage.setAttribute('crossOrigin', 'Anonymous') //url为外域时需要
let imgWidth, imgHeight
newImage.onload = function () {
imgWidth = newImage.width
imgHeight = newImage.height
const canvas = document.createElement('canvas')
const ctx: any = canvas.getContext('2d')
if (Math.max(imgWidth, imgHeight) > w) {
if (imgWidth > imgHeight) {
canvas.width = w
canvas.height = (w * imgHeight) / imgWidth
} else {
canvas.height = w
canvas.width = (w * imgWidth) / imgHeight
}
} else {
canvas.width = imgWidth
canvas.height = imgHeight
quality = 0.8
}
ctx.clearRect(0, 0, canvas.width, canvas.height)
ctx.drawImage(this, 0, 0, canvas.width, canvas.height)
let base64 = canvas.toDataURL('image/jpeg', quality) //压缩语句
// 如想确保图片压缩到自己想要的尺寸,如要求在50-100kb之间,请加以下语句,quality初始值根据情况自定
// while (base64.length / 1024 > 100) {
// quality -= 0.01;
// base64 = canvas.toDataURL("image/jpeg", quality);
// }
// // 防止最后一次压缩低于最低尺寸,只要quality递减合理,无需考虑
// while (base64.length / 1024 < 10) {
// quality += 0.001;
// base64 = canvas.toDataURL("image/jpeg", quality);
// }
callback(base64 as any) //必须通过回调函数返回,否则无法及时拿到该值
}
}
// 获取截图信息
const getCrop = () => {
// 获取截图的 base64 数据
cropper.value.getCropData((data: any) => {
dealImage(data, 300, (res: any) => {
ApiUploadBase64({ image: res })
.then((resData) => {
loading.value = false
emits('update:modelValue', resData.path || '')
ElMessage.success('上传成功')
tailorUpload.value.clearFiles()
closeDialog()
})
.catch(() => {
loading.value = false
})
})
})
}
// 重新上传
const reupload = () => {
uploadBtn.value.ref.click()
}
</script>
<style lang="scss" scoped>
.dialog-footer {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
.reupload {
color: #409eff;
cursor: pointer;
}
}
.avatar-container {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 350px;
background-color: #f0f2f5;
margin-right: 10px;
border-radius: 4px;
.upload {
text-align: center;
margin-bottom: 24px;
}
.avatar-crop {
width: 100%;
height: 350px;
position: relative;
.crop-box {
width: 100%;
height: 100%;
border-radius: 4px;
overflow: hidden;
}
}
}
</style>
+6 -1
View File
@@ -21,7 +21,12 @@ export const ImagePath = (images: any) => {
let newPath: any
if (configStore.asset_url) {
newPath = configStore.asset_url + path
// asset_url 或者path第一位是否带有斜杠
if (path.indexOf('/') === 0 && configStore.asset_url.replace(/\/$/, '').length > 0) {
newPath = configStore.asset_url.slice(0, -1) + path
} else {
newPath = configStore.asset_url + path
}
}
return newPath
}
+9 -9
View File
@@ -1,22 +1,22 @@
<template>
<div class="sidebar-logo sidebar-logo-expend">
<transition enter-active-class="animate__animated animate__fadeInLeft">
<div class="logo_icon" v-if="appStore.sidebarOpened">
<img class="logo_icon_item" v-if="!userStore.userInfo.logo" src="@/assets/menu/logo.png" alt="" />
<img class="logo_icon_item" v-else :src="ImagePath(userStore.userInfo.logo)" alt="" />
<span class="logo-title">{{ userStore.userInfo.site_name || '火星快链' }}</span>
</div>
<div v-else class="logo_icon">
<img class="logo_icon_item" v-if="!userStore.userInfo.logo" src="@/assets/menu/logo.png" alt="" />
<img class="logo_icon_item" v-else :src="ImagePath(userStore.userInfo.logo)" alt="" />
<div class="logo_icon">
<img class="logo_icon_item" v-if="!configStore.web_site_logo" src="@/assets/menu/logo.png" alt="" />
<img class="logo_icon_item" v-else :src="logPath" alt="" />
<span class="logo-title">{{ configStore.web_site_title || '火星快链' }}</span>
</div>
</transition>
</div>
</template>
<script setup lang="ts">
import { appStore, userStore } from '@/stores'
import { configStore } from '@/stores'
import { ImagePath } from '@/hooks/image'
import { computed } from 'vue'
const logPath = computed(() => {
return ImagePath(configStore.web_site_logo)
})
</script>
<style lang="scss" scoped>
+1 -1
View File
@@ -14,7 +14,7 @@ interface WX {
title: string
sub_title: string
qr: QR[]
switch_type: number
switch_type?: number
uv_limit_type: number
}
+17 -1
View File
@@ -1,17 +1,33 @@
import { defineStore } from 'pinia'
import { ApiGetSet } from '@/api/comment'
const useConfigStore = defineStore('init', {
state: () => ({
code_mode: 0,
package: [] as Array<any>,
asset_url: ''
asset_url: '',
web_site_bottom_logo: '',
web_site_customer_service: '',
web_site_logo: '',
web_site_title: ''
}),
actions: {
refresh(res: any) {
this.code_mode = res.code_mode
this.package = res.package
this.asset_url = res.asset_url
this.web_site_bottom_logo = res.web_site_bottom_logo
this.web_site_customer_service = res.web_site_customer_service
this.web_site_logo = res.web_site_logo
this.web_site_title = res.web_site_title
},
// 刷新配置
refreshConfig() {
ApiGetSet().then(res=>{
this.refresh(res)
})
}
}
})
export default useConfigStore
+2 -4
View File
@@ -10,12 +10,10 @@ import { stringify } from 'qs'
import router from '@/router'
import { ElMessage } from 'element-plus'
import { userStore } from '@/stores'
import { VITE_API_URL } from '../../public/config.json'
const http: AxiosInstance = axios.create({
baseURL:
import.meta.env.MODE === 'production'
? `${window.location.origin}/api`
: import.meta.env.VITE_PROXY_PATH,
baseURL: import.meta.env.MODE === 'production' ? VITE_API_URL : import.meta.env.VITE_PROXY_PATH,
timeout: 60000,
headers: {
Accept: 'application/json',
+59 -7
View File
@@ -59,9 +59,24 @@ onMounted(() => {
router.push('/home')
}
})
const getImageUrl = (url: string, path: string) => {
// 判断url最后一位和路径的第一位是否有斜杠
if (url.substr(-1) != '/' && path.substr(0, 1) != '/') {
url = url + '/'
}
// 如果url最后一位有斜杠 或者 路径最后一位有斜杠就去掉一个
if (url.substr(-1) == '/' && path.substr(0, 1) == '/') {
url = url.substr(0, url.length - 1)
}
return url + path
}
</script>
<template>
<div class="login-warp">
<div class="login-warp relative">
<div class="logo flex align-center">
<img class="img" :src="getImageUrl(configStore.asset_url, configStore.web_site_logo)" />
<div class="title">{{ configStore.web_site_title }}</div>
</div>
<div class="login-right">
<!-- 登录页 -->
<div class="login-main">
@@ -70,11 +85,26 @@ onMounted(() => {
<el-icon><Back /></el-icon>
</div>
<h2 style="color: #000">欢迎登录</h2>
<el-form ref="loginFormRef" :disabled="loading.status" :model="loginData" @keyup.enter="submit(loginFormRef)">
<el-form-item prop="username" :rules="[{ required: true, message: '请输入账号', trigger: 'blur' }]">
<el-input v-model="loginData.username" placeholder="请输入账号" prefix-icon="el-icon-user" />
<el-form
ref="loginFormRef"
:disabled="loading.status"
:model="loginData"
@keyup.enter="submit(loginFormRef)"
>
<el-form-item
prop="username"
:rules="[{ required: true, message: '请输入账号', trigger: 'blur' }]"
>
<el-input
v-model="loginData.username"
placeholder="请输入账号"
prefix-icon="el-icon-user"
/>
</el-form-item>
<el-form-item prop="password" :rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]">
<el-form-item
prop="password"
:rules="[{ required: true, message: '请输入密码', trigger: 'blur' }]"
>
<el-input
show-password
v-model="loginData.password"
@@ -83,8 +113,12 @@ onMounted(() => {
/>
</el-form-item>
<el-form-item>
<el-button class="login-btn" type="primary" @click="submit(loginFormRef)" :loading="loading.status"
>立即登录</el-button
<el-button
class="login-btn"
type="primary"
@click="submit(loginFormRef)"
:loading="loading.status"
>立即登录</el-button
>
</el-form-item>
</el-form>
@@ -110,6 +144,24 @@ onMounted(() => {
height: 100vh;
width: 100%;
background-image: url('@/assets/login/login-bg.png');
.logo {
position: absolute;
z-index: 7;
top: 40px;
left: 40px;
.img {
width: 60px;
height: 60px;
border-radius: 9999px;
}
.title {
font-size: 20px;
font-weight: bold;
margin-left: 20px;
color: var(--el-color-primary);
}
}
.login-right {
position: relative;
max-width: 100%;
@@ -4,8 +4,6 @@ import FormModal from '@/components/FormModal.vue'
import CreateOrLook from '@/views/link/addLink/components/app/create-or-look.vue'
import OssImage from '@/components/OssImage.vue'
import { copyLink } from '@/utils'
import { ApiRenewalLink } from '@/api/comment'
import { ElMessage, ElMessageBox } from 'element-plus'
import useUserStore from '@/stores/modules/user'
const user = useUserStore()
@@ -20,24 +18,7 @@ const { state, sizeChangeHandle, currentChangeHandle, showForm, submitForm, rese
username: ''
}
})
// 续费
const renewal = (id: number) => {
ElMessageBox.confirm('确定要续费一个月吗?这将要扣除1个创建个数', 'Warning', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
center: true
})
.then(() => {
ApiRenewalLink({
id: id
}).then(() => {
ElMessage.success('操作成功!')
research()
})
})
.catch(() => {})
}
</script>
<template>
@@ -74,9 +55,9 @@ const renewal = (id: number) => {
:row-key="state.primaryKey"
>
<el-table-column prop="id" label="序号"></el-table-column>
<el-table-column label="图">
<el-table-column label="图" >
<template #default="{ row }">
<oss-image :paths="row.icon ? [row.icon] : []" :width="60" :height="60"></oss-image>
<oss-image :paths="row.icon ? [row.icon] : []" :width="60" :height="60" style="margin: 0 auto"></oss-image>
</template>
</el-table-column>
<el-table-column prop="title" label="卡片标题"></el-table-column>
@@ -18,24 +18,7 @@ const { state, sizeChangeHandle, currentChangeHandle, showForm, submitForm, rese
page: 1
}
})
// 续费
const renewal = (id: number) => {
ElMessageBox.confirm('确定要续费一个月吗?这将要扣除1个创建个数', 'Warning', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
center: true
})
.then(() => {
ApiRenewalLink({
id: id
}).then(() => {
ElMessage.success('操作成功!')
research()
})
})
.catch(() => {})
}
const user = useUserStore()
</script>
@@ -73,9 +56,9 @@ const user = useUserStore()
:row-key="state.primaryKey"
>
<el-table-column prop="id" label="序号"></el-table-column>
<el-table-column label="图">
<el-table-column label="图">
<template #default="{ row }">
<oss-image :paths="row.icon ? [row.icon] : []" :width="60" :height="60"></oss-image>
<oss-image :paths="row.icon ? [row.icon] : []" :width="60" :height="60" style="margin: 0 auto"></oss-image>
</template>
</el-table-column>
<el-table-column prop="title" label="卡片标题"></el-table-column>
@@ -18,24 +18,7 @@ const { state, sizeChangeHandle, currentChangeHandle, showForm, submitForm, rese
page: 1
}
})
// 续费
const renewal = (id: number) => {
ElMessageBox.confirm('确定要续费一个月吗?这将要扣除1个创建个数', 'Warning', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
center: true
})
.then(() => {
ApiRenewalLink({
id: id
}).then(() => {
ElMessage.success('操作成功!')
research()
})
})
.catch(() => {})
}
const user = useUserStore()
</script>
@@ -73,9 +56,9 @@ const user = useUserStore()
:row-key="state.primaryKey"
>
<el-table-column prop="id" label="序号"></el-table-column>
<el-table-column label="图">
<el-table-column label="图">
<template #default="{ row }">
<oss-image :paths="row.icon ? [row.icon] : []" :width="60" :height="60"></oss-image>
<oss-image :paths="row.icon ? [row.icon] : []" :width="60" :height="60" style="margin: 0 auto"></oss-image>
</template>
</el-table-column>
<el-table-column prop="title" label="卡片标题"></el-table-column>
@@ -18,24 +18,7 @@ const { state, sizeChangeHandle, currentChangeHandle, showForm, submitForm, rese
page: 1
}
})
// 续费
const renewal = (id: number) => {
ElMessageBox.confirm('确定要续费一个月吗?这将要扣除1个创建个数', 'Warning', {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
center: true
})
.then(() => {
ApiRenewalLink({
id: id
}).then(() => {
ElMessage.success('操作成功!')
research()
})
})
.catch(() => {})
}
const user = useUserStore()
</script>
@@ -72,9 +55,9 @@ const user = useUserStore()
:row-key="state.primaryKey"
>
<el-table-column prop="id" label="序号"></el-table-column>
<el-table-column label="图">
<el-table-column label="图">
<template #default="{ row }">
<oss-image :paths="row.icon ? [row.icon] : []" :width="60" :height="60"></oss-image>
<oss-image :paths="row.icon ? [row.icon] : []" :width="60" :height="60" style="margin: 0 auto"></oss-image>
</template>
</el-table-column>
<el-table-column prop="title" label="卡片标题"></el-table-column>
@@ -0,0 +1,184 @@
<script setup lang="ts">
import { QuestionFilled } from '@element-plus/icons-vue'
import MaterialSelect from '@/components/MaterialSelect/MaterialSelect.vue'
import useForm from '@/hooks/form'
import { onMounted, ref } from 'vue'
import { ApiDomainEnableList } from '@/api/domain'
import { ApiAppList } from '@/api/app'
import type { linkConfig } from '@/models/link'
import OssImage from '@/components/OssImage.vue'
import UploadImage from '@/components/uploadImage.vue'
import QrLink from '@/views/link/addLink/components/qr/qrLink.vue'
import { configStore, userStore } from '@/stores'
import { getNextMonth } from '@/utils/dateTime'
const formRef = ref()
const { formData, formLoading } = useForm({
formRef,
defVal(detail) {
return {
title: detail?.title, // 标题
type: '11', // 类型
description: detail?.description, // 描述
icon: detail?.icon, // 封面图
remark: detail?.remark, // 备注
config: detail?.config ? detail?.config : ({} as linkConfig) // 无风险域名 / 小程序 / 地址
}
}
})
// 获取域名列表
const domainList = ref()
const getDomainList = async () => {
domainList.value = await ApiDomainEnableList()
}
// 获取小程序列表
const miniProgramList = ref()
const getMiniProgramList = async () => {
miniProgramList.value = await ApiAppList()
}
const rules = {
title: [{ required: true, message: '请输入标题', trigger: 'blur' }],
'config.domain_id': [{ required: true, message: '请选择域名', trigger: 'blur' }],
// 'config.min_id': [{ required: true, message: '请选择小程序', trigger: 'blur' }],
'config.url': [{ required: true, message: '请选择二维码图片', trigger: 'blur' }]
}
const userInfo = userStore
onMounted(() => {
getDomainList()
getMiniProgramList()
})
</script>
<template>
<div style="margin-top: -30px" class="m-b-20">
<el-space class="text-main">
<span>总个数{{ userInfo.userInfo.link_amount }}</span>
<span>消耗个数1</span>
</el-space>
</div>
<div class="main">
<el-form
:model="formData"
:rules="rules"
ref="formRef"
v-loading="formLoading"
class="form-box margin-right-40"
label-position="top"
>
<el-form-item prop="title" label="卡片标题">
<el-input v-model="formData.title" placeholder="请输入卡片标题"></el-input>
</el-form-item>
<el-form-item label="卡片描述">
<el-input v-model="formData.description" placeholder="请输入卡片描述"></el-input>
</el-form-item>
<el-form-item label="卡片封面图">
<div class="item-box">
<oss-image
class="margin-right-20"
:paths="formData.icon ? [formData.icon] : []"
:width="100"
:height="100"
></oss-image>
<div>
<div class="function-img">
<upload-image
class="margin-right-10"
:type="['image/jpg', 'image/jpeg', 'image/png']"
:size="1"
v-model="formData.icon"
></upload-image>
<material-select v-model="formData.icon" class="margin-right-20"></material-select>
</div>
<span class="tip">只能上传jpg/png建议大小100*100且不超过1Mb</span>
</div>
</div>
</el-form-item>
<el-form-item label="卡片备注">
<el-input v-model="formData.remark" placeholder="请输入卡片备注,例如:xxx活动(选填)"></el-input>
</el-form-item>
<el-form-item prop="config.domain_id" label="无风险域名">
<el-select v-model="formData.config.domain_id" placeholder="请选择">
<el-option v-for="item in domainList" :key="item.id" :label="item.title" :value="item.id"></el-option>
</el-select>
</el-form-item>
<el-form-item prop="config.url" label="优码二维码">
<qr-link class="margin-b-10" v-model="formData.config.url"></qr-link>
<el-input v-model="formData.config.url" disabled></el-input>
<div style="font-size: 12px">
<el-icon><QuestionFilled /></el-icon>
上传在优码图片类使用微信二维码公众号二维码企业微信二维码视频号二维码生成的二维码<br />
</div>
</el-form-item>
</el-form>
<div class="show hidden-md-and-down">
<el-divider>
<span>抖音卡片预览</span>
</el-divider>
<div class="show-box">
<oss-image
class="margin-right-20"
:paths="formData.icon ? [formData.icon] : []"
:width="100"
:height="100"
></oss-image>
<div class="show-des">
<span class="show-des-t">{{ formData.title || '卡片标题' }}</span>
<span class="show-des-b">{{ formData.description || '卡片描述' }}</span>
</div>
</div>
</div>
</div>
</template>
<style scoped lang="scss">
.main {
display: flex;
.form-box {
flex: 2;
}
.show {
flex: 1;
}
}
.tip {
color: #999;
}
.show-box {
display: flex;
width: 100%;
height: 100px;
border: 1px solid #f5f5f7;
background-color: #f9fafe;
}
.show-des {
flex: 1;
display: flex;
flex-direction: column;
justify-content: center;
.show-des-t {
font-size: 20px;
color: #333;
}
.show-des-b {
font-size: 14px;
color: #999;
}
}
.item-box {
display: flex;
align-items: center;
}
.function-img {
margin-top: 10px;
display: flex;
align-items: center;
}
</style>
@@ -0,0 +1,120 @@
<script setup lang="ts">
import useCurd from '@/hooks/curd'
import FormModal from '@/components/FormModal.vue'
import CreateOrLook from './create-or-look.vue'
import OssImage from '@/components/OssImage.vue'
import { copyLink } from '@/utils'
import { ApiRenewalLink } from '@/api/comment'
import { ElMessage, ElMessageBox } from 'element-plus'
import useUserStore from '@/stores/modules/user'
const user = useUserStore()
const props = defineProps<{
type: any
}>()
const { state, sizeChangeHandle, currentChangeHandle, showForm, submitForm, research, deleteHandle } = useCurd({
url: '/links',
queryForm: {
type: props.type,
page: 11,
username: ''
}
})
</script>
<template>
<div>
<el-row :gutter="20">
<el-col :xs="24" :sm="8" :md="8" :lg="6" class="m-b-20">
<el-input style="width: 100%" v-model="state.queryForm.title" placeholder="请输入标题" clearable></el-input>
</el-col>
<el-col :xs="24" :sm="8" :md="8" :lg="6" class="m-b-20" v-if="user.userInfo.type === 3">
<el-input
style="width: 100%"
placeholder="请输入用户名"
v-model="state.queryForm.username"
clearable
></el-input>
</el-col>
<el-col :xs="24" :sm="8" :md="8" :lg="6" class="m-b-20">
<el-button type="primary" @click="research">搜索</el-button>
<el-button type="primary" :plain="true" @click="showForm">创建卡片</el-button>
</el-col>
</el-row>
<!-- 表格 -->
<el-table
:data="state.dataList"
v-loading="state.listLoading"
:header-cell-style="{
'text-align': 'center',
'background-color': '#F1F1F1',
height: '50px',
color: '#333'
}"
:cell-style="{ padding: '0', 'text-align': 'center', height: '80px', color: '#333' }"
:row-key="state.primaryKey"
>
<el-table-column prop="id" label="序号"></el-table-column>
<el-table-column label="图片">
<template #default="{ row }">
<oss-image :paths="row.icon ? [row.icon] : []" :width="60" :height="60" style="margin: 0 auto"></oss-image>
</template>
</el-table-column>
<el-table-column prop="title" label="卡片标题"></el-table-column>
<el-table-column prop="description" label="卡片描述"></el-table-column>
<el-table-column prop="" label="链接状态">
<template #default="{ row }">
<span v-if="row.status == 1">可用</span>
<span v-else style="color: red">不可用</span>
</template>
</el-table-column>
<el-table-column prop="visit_uv_count" label="浏览量UV"></el-table-column>
<el-table-column prop="expired_at" label="到期时间"></el-table-column>
<el-table-column prop="user.username" label="所属用户"></el-table-column>
<el-table-column label="操作">
<template #default="{ row }">
<el-button type="primary" link @click="copyLink(row)">复制链接</el-button>
<el-button type="primary" link @click="showForm(row.id)">编辑</el-button>
<el-button type="primary" link @click="deleteHandle(row.id)">删除</el-button>
</template>
</el-table-column>
</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"
/>
<!-- 弹窗 -->
<form-modal
v-if="state.formVisible === true"
v-model:visible="state.formVisible"
width="1200"
:style="{
minWidth: '700px'
}"
type="dialog"
:title="state.detailData?.id ? '编辑' : '新增'"
:loading="state.formLoading"
:detail="state.detailData"
@submit="submitForm"
cancelBtnName="取消"
okBtnName="确定"
>
<create-or-look></create-or-look>
</form-modal>
</div>
</template>
<style scoped lang="scss">
.header-right {
float: right;
}
</style>
+6 -1
View File
@@ -4,12 +4,14 @@ import WX from '@/views/link/addLink/components/wx/index.vue'
import KingDoc from '@/views/link/addLink/components/kingDoc/index.vue'
import QR from '@/views/link/addLink/components/qr/index.vue'
import FirmWx from '@/views/link/addLink/components/firmWx/index.vue'
import TengXun from '@/views/link/addLink/components/tengxun/index.vue'
import { onMounted, ref } from 'vue'
import xcx from '@/assets/link/xcx.png'
import jinshan from '@/assets/link/jinshan.png'
import caoliao from '@/assets/link/caoliao.png'
import qywx from '@/assets/link/qywx.png'
import wx from '@/assets/link/wx.png'
import tengxun from '@/assets/link/tengxun.png'
import { userStore } from '@/stores'
const tabsList = [
@@ -17,7 +19,9 @@ const tabsList = [
{ id: 2, name: '跳转到金山文档', icon: jinshan },
{ id: 3, name: '跳转到草料二维码', icon: caoliao },
{ id: 4, name: '跳转到企业微信', icon: qywx },
{ id: 1, name: '跳转到小程序', icon: xcx }
{ id: 1, name: '跳转到小程序', icon: xcx },
{ id: 11, name: '跳转到腾讯优码', icon: tengxun },
]
const user = userStore
// tabs点击事件
@@ -53,6 +57,7 @@ onMounted(() => {
<q-r v-if="type === 3" :type="type"></q-r>
<firm-wx v-if="type === 4" :type="type"></firm-wx>
<w-x v-if="type === 5" :type="type"></w-x>
<teng-xun v-if="type === 11" :type="type"></teng-xun>
</el-card>
</template>
+275 -36
View File
@@ -3,12 +3,17 @@ import { onMounted, ref } from 'vue'
import { ApiGetSetByType, ApiSaveSetByType } from '@/api/comment'
import { ApiVipList } from '@/api/super'
import { ElMessage } from 'element-plus'
import UploadImage from '@/components/uploadImage.vue'
import MaterialSelect from '@/components/MaterialSelect/MaterialSelect.vue'
import OssImage from '@/components/OssImage.vue'
import { configStore } from '@/stores'
const formData = ref<any>({})
const vipPackages = ref<any>([])
const formLoading = ref(false)
const webFormData = ref<any>({})
const clickSubmit = () => {
formLoading.value = true
@@ -20,6 +25,62 @@ const clickSubmit = () => {
formLoading.value = false
})
}
const saveSubmit = () => {
if (webFormData.value.verify_code_is_open) {
if (!webFormData.value.send_code_mode ){
ElMessage.error('请选择发送方式')
return
}
if (webFormData.value.send_code_mode === 1) {
if (!webFormData.value.ali_sms_key) {
ElMessage.error('请填写阿里短信key')
return
}
if (!webFormData.value.ali_sms_secret) {
ElMessage.error('请填写阿里短信secret')
return
}
if (!webFormData.value.ali_sms_secret) {
ElMessage.error('请填写阿里短信secret')
return
}
} else if (webFormData.value === 2) {
if (!webFormData.value.mail_from_name) {
ElMessage.error('请填写发信名称')
return
}
if (!webFormData.value.mail_host) {
ElMessage.error('请填写服务器地址')
return
}
if (!webFormData.value.mail_port) {
ElMessage.error('请填写端口')
return
}
if (!webFormData.value.mail_from_address) {
ElMessage.error('请填写发信地址')
return
}
if (!webFormData.value.mail_username) {
ElMessage.error('请填写邮箱账号')
return
}
if (!webFormData.value.mail_password) {
ElMessage.error('请填写邮箱密码')
return
}
}
}
formLoading.value = true
ApiSaveSetByType('web_site', webFormData.value)
.then(() => {
ElMessage.success('保存成功')
configStore.refreshConfig()
})
.finally(() => {
formLoading.value = false
})
}
onMounted(() => {
formLoading.value = true
@@ -30,6 +91,9 @@ onMounted(() => {
.finally(() => {
formLoading.value = false
})
ApiGetSetByType('web_site').then((res: any) => {
webFormData.value = res
})
ApiVipList().then((res: any) => {
vipPackages.value = vipPackages.value.concat(res)
})
@@ -37,43 +101,218 @@ onMounted(() => {
</script>
<template>
<el-card header="网站设置" v-loading="formLoading">
<el-form label-width="200px" :disabled="formLoading">
<div>
<el-card title="基础配置">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="8">
<div class="m-b-20">顶部LOGO图片信息</div>
<div class="item-box">
<oss-image
class="margin-right-20"
:paths="webFormData.web_site_logo ? [webFormData.web_site_logo] : []"
:width="100"
:height="100"
></oss-image>
<div>
<div class="function-img m-b-20">
<upload-image
class="margin-right-10"
:type="['image/jpg', 'image/jpeg', 'image/png']"
:size="1"
v-model="webFormData.web_site_logo"
></upload-image>
<material-select
v-model="webFormData.web_site_logo"
class="margin-right-20"
></material-select>
</div>
<span class="tip">只能上传jpg/png建议大小100*100且不超过1Mb</span>
</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<div class="m-b-20">底部LOGO图片信息</div>
<div class="item-box">
<oss-image
class="margin-right-20"
:paths="webFormData.web_site_bottom_logo ? [webFormData.web_site_bottom_logo] : []"
:width="100"
:height="100"
></oss-image>
<div>
<div class="function-img m-b-20">
<upload-image
class="margin-right-10"
:type="['image/jpg', 'image/jpeg', 'image/png']"
:size="1"
v-model="webFormData.web_site_bottom_logo"
></upload-image>
<material-select v-model="webFormData.web_site_bottom_logo" class="margin-right-20"></material-select>
</div>
<span class="tip">只能上传jpg/png且不超过1Mb</span>
</div>
</div>
</el-col>
<el-col :xs="24" :sm="12" :md="8">
<div class="m-b-20">上传客服二维码</div>
<div class="item-box">
<oss-image
class="margin-right-20"
:paths="webFormData.web_site_customer_service ? [webFormData.web_site_customer_service] : []"
:width="100"
:height="100"
></oss-image>
<div>
<div class="function-img m-b-20">
<upload-image
class="margin-right-10"
:type="['image/jpg', 'image/jpeg', 'image/png']"
:size="1"
v-model="webFormData.web_site_customer_service"
></upload-image>
<material-select
v-model="webFormData.web_site_customer_service"
class="margin-right-20"
></material-select>
</div>
<span class="tip">只能上传jpg/png建议大小100*100且不超过1Mb</span>
</div>
</div>
</el-col>
<el-form-item label="注册赠送会员">
<el-select v-model="formData.give_vip_id" placeholder="注册赠送会员">
<el-option style="width: 200px" v-for="vip in vipPackages" :key="vip.id" :label="vip.name" :value="vip.id" />
</el-select>
</el-form-item>
<el-form-item label="是否开启赠送会员">
<el-switch style="width: 200px" v-model="formData.is_give_vip" ></el-switch>
</el-form-item>
<el-form-item label="注册赠送天数">
<el-input-number style="width: 200px" v-model="formData.give_vip_days" :min="1" :max="365" />
</el-form-item>
<el-form-item label="验证码发送方式">
<el-select style="width: 200px" v-model="formData.send_code_mode" placeholder="请选择验证码发送方式">
<el-option label="短信" :value="1" />
<el-option label="邮箱" :value="2" />
</el-select>
</el-form-item>
<el-form-item label="一级邀请注册佣金">
<el-input-number style="width: 200px" v-model="formData.recommend_commission_1" :min="0" />
</el-form-item>
<el-form-item label="二级邀请注册佣金">
<el-input-number style="width: 200px" v-model="formData.recommend_commission_2" :min="0" />
</el-form-item>
<el-form-item label="一级分销(%">
<el-input-number style="width: 200px" v-model="formData.member_pay_commission_1" />
</el-form-item>
<el-form-item label="二级分销(%">
<el-input-number style="width: 200px" v-model="formData.member_pay_commission_2" />
</el-form-item>
<el-form-item>
<el-col :xs="24" :sm="12" :md="6">
<div class="margin-b-10" style="margin-top: 20px">网站标题</div>
<el-input v-model="webFormData.web_site_title" placeholder="请输入网站标题"></el-input>
</el-col>
<el-divider content-position="left">验证码</el-divider>
<el-col :xs="24" :sm="12" :md="6">
<div class="margin-b-10" style="margin-top: 20px">是否开启验证码</div>
<el-switch style="width: 100%" :active-value="1" :inactive-value="0" v-model="webFormData.verify_code_is_open"></el-switch>
</el-col>
<el-col :xs="24" :sm="12" :md="6" v-if="webFormData.verify_code_is_open">
<div class="margin-b-10" style="margin-top: 20px">验证码发送方式</div>
<el-select style="width: 100%" v-model="webFormData.send_code_mode" placeholder="请选择验证码发送方式">
<el-option label="短信" :value="1" />
<el-option label="邮箱" :value="2" />
</el-select>
</el-col>
<el-col :xs="24" :sm="12" :md="6" v-if="webFormData.send_code_mode === 1">
<div class="margin-b-10" style="margin-top: 20px">阿里短信key</div>
<el-input v-model="webFormData.ali_sms_key" placeholder="请输入阿里短信key"></el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="6" v-if="webFormData.send_code_mode === 1">
<div class="margin-b-10" style="margin-top: 20px">阿里短信secret</div>
<el-input v-model="webFormData.ali_sms_secret" placeholder="请输入阿里短信secret"></el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="6" v-if="webFormData.send_code_mode === 1">
<div class="margin-b-10" style="margin-top: 20px">阿里短信签名</div>
<el-input v-model="webFormData.ali_sms_sign_name" placeholder="请输入阿里短信签名"></el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="6" v-if="webFormData.send_code_mode === 2">
<div class="margin-b-10" style="margin-top: 20px">发信名称</div>
<el-input v-model="webFormData.mail_from_name" placeholder="请输入发信名称"></el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="6" v-if="webFormData.send_code_mode === 2">
<div class="margin-b-10" style="margin-top: 20px">服务器地址</div>
<el-input v-model="webFormData.mail_host" placeholder="请输入服务器地址"></el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="6" v-if="webFormData.send_code_mode === 2">
<div class="margin-b-10" style="margin-top: 20px">端口</div>
<el-input v-model="webFormData.mail_port" placeholder="端口"></el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="6" v-if="webFormData.send_code_mode === 2">
<div class="margin-b-10" style="margin-top: 20px">发信地址</div>
<el-input v-model="webFormData.mail_from_address" placeholder="请输入发信地址"></el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="6" v-if="webFormData.send_code_mode === 2">
<div class="margin-b-10" style="margin-top: 20px">发信账号</div>
<el-input v-model="webFormData.mail_username" placeholder="请输入发信账号"></el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="6" v-if="webFormData.send_code_mode === 2">
<div class="margin-b-10" style="margin-top: 20px">发信密码</div>
<el-input v-model="webFormData.mail_password" placeholder="请输入发信密码"></el-input>
</el-col>
<el-divider content-position="left"> 支付配置</el-divider>
<el-col :xs="24" :sm="12" :md="6">
<div class="margin-b-10" style="margin-top: 20px">微信商户appId</div>
<el-input v-model="webFormData.wechat_pay_app_id" placeholder="请输入微信商户appId"></el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="margin-b-10" style="margin-top: 20px">微信商户号</div>
<el-input v-model="webFormData.wechat_pay_mch_id" placeholder="请输入微信商户号"></el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="margin-b-10" style="margin-top: 20px">微信支付密钥</div>
<el-input v-model="webFormData.wechat_pay_secret_key" placeholder="请输入微信支付密钥"></el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="margin-b-10" style="margin-top: 20px">微信支付私钥证书</div>
<el-input v-model="webFormData.wechat_pay_private_cert" placeholder="请输入微信支付私钥证书"></el-input>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="margin-b-10" style="margin-top: 20px">微信支付公钥证书</div>
<el-input v-model="webFormData.wechat_pay_certificate" placeholder="请输入微信支付公钥证书"></el-input>
</el-col>
</el-row>
<div class="text-center m-t-30">
<el-button type="primary" style="width: 200px" @click="saveSubmit" :loading="formLoading">保存</el-button>
</div>
</el-card>
<el-card v-loading="formLoading" title="分销配置" class="m-t-20">
<el-form label-width="200px" :disabled="formLoading">
<el-row :gutter="20">
<el-col :xs="24" :sm="12" :md="6">
<div class="margin-b-10" style="margin-top: 20px">是否开启赠送会员</div>
<el-switch style="width: 100%" v-model="formData.is_give_vip"></el-switch>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<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>
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<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" />
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="margin-b-10" style="margin-top: 20px">一级邀请注册佣金</div>
<el-input-number style="width: 100%" v-model="formData.recommend_commission_1" :min="0" />
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="margin-b-10" style="margin-top: 20px">二级邀请注册佣金</div>
<el-input-number style="width: 100%" v-model="formData.recommend_commission_2" :min="0" />
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="margin-b-10" style="margin-top: 20px">一级分销%</div>
<el-input-number style="width: 100%" v-model="formData.member_pay_commission_1" />
</el-col>
<el-col :xs="24" :sm="12" :md="6">
<div class="margin-b-10" style="margin-top: 20px">二级分销%</div>
<el-input-number style="width: 100%" v-model="formData.member_pay_commission_2" />
</el-col>
</el-row>
</el-form>
<div class="text-center m-t-30">
<el-button type="primary" style="width: 200px" @click="clickSubmit" :loading="formLoading">保存</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
</el-card>
</div>
</template>
<style scoped lang="scss"></style>
<style scoped lang="scss">
.item-box {
display: flex;
align-items: center;
}
.function-img {
margin-top: 10px;
display: flex;
align-items: center;
}
</style>
+3 -2
View File
@@ -1,6 +1,6 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"include": ["env.d.ts", "src/**/*", "src/**/*.vue","./public/config.json"],
"exclude": ["src/**/__tests__/*"],
"compilerOptions": {
"composite": true,
@@ -9,6 +9,7 @@
"@/*": ["./src/*"]
},
"types": ["element-plus/global"],
"moduleResolution": "node"
"moduleResolution": "node",
"resolveJsonModule": true,
}
}
-20
View File
@@ -52,26 +52,6 @@ class SystemInit extends Command
private function config_data(): array
{
return [
[
'slug' => 'web_site_title',
'value' => '蚂蚁快链-智慧引流工具',
'desc' => '网站名称',
],
[
'slug' => 'web_site_logo',
'value' => '/image/toplogo.png',
'desc' => '网站logo浅色',
],
[
'slug' => 'web_site_bottom_logo',
'value' => '/image/toplogo.png',
'desc' => '网站logo深色',
],
[
'slug' => 'web_site_customer_service',
'value' => '/image/web-qr.png',
'desc' => '客服二维码',
],
[
'slug' => 'is_give_vip',
'value' => 1,
-1
View File
@@ -10,7 +10,6 @@ enum LinkType: int
case CLI_QR = 3;
case WORK_WECHAT = 4;
case LANDING_MINI = 5;
case QR_QQ = 11; // 腾讯优码
// 获取所有的类型
public static function getAllType(): array
+1
View File
@@ -19,6 +19,7 @@ class BaseConfig implements SimpleForm
'recommend_commission_2' => 'nullable|integer',
'member_pay_commission_1' => 'nullable|integer', // 一级邀请人消费提成 1%
'member_pay_commission_2' => 'nullable|integer', // 二级邀请人消费提成 1%
'send_code_mode' => 'required|integer|in:1,2', // 1短信2邮箱
]);
}
-75
View File
@@ -1,75 +0,0 @@
<?php
namespace App\Forms;
use App\Services\SystemConfig;
use Illuminate\Http\Request;
use Ugly\Base\Contracts\SimpleForm;
class WebSite implements SimpleForm
{
public function policy(Request $request): array
{
return $request->validate([
'web_site_customer_service' => 'nullable',
'web_site_title' => 'nullable',
'web_site_logo' => 'nullable',
'web_site_bottom_logo' => 'nullable',
'verify_code_is_open' => 'required|boolean',
'send_code_mode' => 'nullable|integer|in:1,2', // 1短信2邮箱
'ali_sms_key' => 'nullable',
'ali_sms_secret' => 'nullable',
'ali_sms_sign_name' => 'nullable',
'mail_from_name' => 'nullable',
'mail_host' => 'nullable',
'mail_port' => 'nullable',
'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',
]);
}
// 保存
public function handle(array $input): void
{
SystemConfig::set($input);
}
public function default(): array
{
return [
'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'),
'web_site_bottom_logo' => SystemConfig::get('web_site_bottom_logo'),
'verify_code_is_open' => SystemConfig::get('verify_code_is_open'), // 开启
'send_code_mode' => SystemConfig::get('send_code_mode'), //
'ali_sms_key' => SystemConfig::get('ali_sms_key'),
'ali_sms_secret' => SystemConfig::get('ali_sms_secret'),
'ali_sms_sign_name' => SystemConfig::get('ali_sms_sign_name'),
'mail_from_name' => SystemConfig::get('mail_from_name'),
'mail_host' => SystemConfig::get('mail_host'),
'mail_port' => SystemConfig::get('mail_port'),
'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,13 +3,11 @@
namespace App\Http\Controllers\Api;
use App\Forms\BaseConfig;
use App\Forms\WebSite;
use Ugly\Base\Http\Controllers\SimpleFormController;
class ConfigController extends SimpleFormController
{
protected array $form = [
'base' => BaseConfig::class,
'web_site' => WebSite::class,
];
}
@@ -114,22 +114,19 @@ class IndexController extends Controller
$configs = [
'code_mode' => SystemConfig::get('send_code_mode'),
'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'),
'web_site_bottom_logo' => SystemConfig::get('web_site_bottom_logo'),
'asset_url' => Storage::url(''), // 图片路径
];
$user = auth('api')->user();
if ($user) {
// 图片路径
$configs['asset_url'] = Storage::url('');
// 安全域名
// $configs['domains'] = Domain::query()->where('enable', true)->get(['id', 'title']);
// 小程序 (区分是否有权限使用平台小程序池)
$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))
fn($query) => $query->where('user_id', $user->id)
->when($user->getPackageConfig('pre_min'), fn($query) => $query->orWhere('is_pre_min', true))
)
->get(['id', 'name']);
}
@@ -1,319 +0,0 @@
<?php
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;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;
use Ugly\Base\Traits\ApiResource;
class JumpController extends Controller
{
use ApiResource;
// 获取链接跳转目标
public function target(Request $request, $code): JsonResponse
{
$link = Link::query()->where('code', $code)->first();
if (empty($link)) {
return $this->failed('此链接已被删除!');
}
if ($link->expired_at < now()) {
return $this->failed('链接已过期!');
}
$user = User::query()->findOrFail($link->user_id);
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()
->where('link_id', $link->id)
->where('device_uid', $device_uid)
->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,
'icon' => Storage::url($link->icon),
];
if (
$link->type === LinkType::MINI_PROGRAM ||
$link->type === LinkType::LANDING_MINI
) {
// 跳转到小程序 参数
$mp = MiniProgram::query()->findOrFail(data_get($link, 'config.min_id'));
if (empty($mp) || ! $mp->is_enable) {
return $this->failed('当前小程序不能使用');
}
if ($link->type === LinkType::LANDING_MINI) {
$qr = $this->getWechatNextQr($link);
if (empty($qr)) {
return $this->failed('当前小程序已无可用二维码!');
}
}
$cache['params'] = [
'appid' => $mp->app_id,
'secret' => $mp->secret,
'qr' => $qr ?? null,
'path' => data_get($link, 'config.url') ?: $mp->url,
];
} elseif ($link->type === LinkType::KING_DOC) {
$cache['params']['sid'] = str_replace('https://kdocs.cn/l/', '', data_get($link, 'config.url'));
} elseif ($link->type === LinkType::CLI_QR) {
/**
* 1、电脑端访问草料二维码官网:https://cli.im/img
* 2、上传微信二维码、公众号二维码、企业微信二维码、视频号二维码等等,然后点生成活码
* 3、后台创建草料
*/
$configUrl = str_replace('https://qr61.cn/', '', data_get($link, 'config.url'));
[$userID, $cliID] = explode('/', $configUrl);
$cache['params'] = [
'userID' => $userID,
'cliID' => $cliID,
];
} elseif ($link->type === LinkType::WORK_WECHAT) {
$cache['params'] = [
'url' => data_get($link, 'config.url'),
];
} elseif ($link->type === LinkType::QR_QQ) {
$cache['params'] = [
'url' => data_get($link, 'config.url'),
];
} else {
return $this->failed('暂不支持此链接跳转!');
}
} else {
unset($cache['target']);
}
// 生成链接
if ($link->type === LinkType::WORK_WECHAT) {
$cache['target'] = data_get($cache, 'params.url');
} elseif ($link->type === LinkType::CLI_QR) {
$userID = data_get($cache, 'params.userID');
$cliID = data_get($cache, 'params.cliID');
$getTicketUrl = file_get_contents('https://nc.cli.im/api/weixin/getWxUrlScheme/?query=q=qr61.cn/'.$userID.'/'.$cliID.'&path=pages/code/code&appid=wx5db79bd23a923e8e&org_coding='.$userID);
$fetchUrlData = file_get_contents(data_get(json_decode($getTicketUrl, true), 'data.wx_url_scheme.fetchUrl'));
$arr = json_decode($fetchUrlData, true);
$cache['target'] = data_get($arr, 'data.urlScheme');
} elseif ($link->type === LinkType::QR_QQ) {
$configUrl = data_get($cache, 'params.url');
$getContent = file_get_contents($configUrl);
$pattern = '/weixin:\/\/dl\/business\/\?t=[^\\\']+/';
preg_match($pattern, $getContent, $matches);
$cache['target'] = $matches[0];
} elseif ($link->type === LinkType::KING_DOC) {
$sidUrl = data_get($cache, 'params.sid');
preg_match('/https:\/\/kdocs\.cn\/l\/([a-zA-Z0-9]+)/', $sidUrl, $matches);
$url_link = Http::withUserAgent('Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36')
->get('https://account.kdocs.cn/api/v3/miniprogram/urllink', [
'appid' => 'wx5b97b0686831c076',
'path' => 'pages/navigate/navigate',
'query' => http_build_query([
'url' => 'pages/preview/preview?from=wxminiprogram&fid=256465035925&sid='.$matches[1].'&fname='.urlencode('扫码加微信.docx'),
'scene' => '102',
'jump_from' => 'wechatlogin_guide_passive',
'comp' => 'docx',
'dw' => '1',
]),
'env_version' => 'release',
'is_expire' => 'true',
'expire_time' => time() + 7200,
])->json('url_link');
$jinShanResponse = Http::get($url_link)->throw();
preg_match("/url_scheme:\s*'([^']+)'/", $jinShanResponse, $matches);
$cache['target'] = $matches[1];
} else {
// 小程序
$miniApp = new WechatApp([
'app_id' => data_get($cache, 'params.appid'),
'secret' => data_get($cache, 'params.secret'),
]);
try {
$res = $miniApp->getClient()->postJson('wxa/generatescheme', [
'jump_wxa' => [
'path' => data_get($cache, 'params.path'),
'query' => 'code='.data_get($link, 'code').'&device_uid='.$device_uid,
],
])
->toArray();
if (isset($res['errcode']) && $res['errcode'] === 40001) {
$cache = new Psr16Cache(new FilesystemAdapter('easywechat', 1500));
$cache->clear();
}
} catch (\Exception $e) {
$res = [
'errcode' => 1,
'errmsg' => '小程序配置错误',
];
}
if (! empty($res['errcode'])) {
return $this->failed($res['errmsg']);
}
$cache['target'] = data_get($res, 'openlink');
}
if (isset($cache['target']) && $cache['target']) {
LinkVisitLog::query()->create([
'link_id' => $link->id,
'user_id' => $link->user_id,
'ip' => $request->getClientIp(),
'device_uid' => $device_uid,
'cache' => $cache,
]);
unset($cache['params']);
return $this->success($cache);
} else {
return $this->failed('访问错误!');
}
}
// 获取需要展示的二维码
public function getShowQr($code): JsonResponse
{
$link = Link::query()->where('code', $code)->first();
if (empty($link) || ! $link->status) {
return $this->success([
'id' => '',
'avatar' => '',
'title' => '数据不存在~',
'sub_title' => '',
'qr' => '',
]);
}
$config = data_get($link, 'config.wx');
$device_uid = request()->input('device_uid') ?: Str::uuid();
$cache_qr = data_get(LinkVisitLog::query()
->where('link_id', $link->id)
->where('device_uid', $device_uid)
->orderByDesc('id')
->first(), 'cache.params.qr', []);
if (empty($cache_qr)) {
return $this->failed('参数错误!');
}
return $this->success([
'id' => $link->id,
'avatar' => Storage::url(data_get($config, 'avatar')),
'title' => data_get($config, 'title'),
'sub_title' => data_get($config, 'sub_title'),
'qr' => Storage::url(data_get($cache_qr, 'path')),
]);
}
// 获取下一次访问时展示的二维码
private function getWechatNextQr($link)
{
$qrCodes = data_get($link, 'config.wx.qr', []);
// 对链接中的二维码们按照 sort 从小到大排序
usort($qrCodes, function ($a, $b) {
return $a['sort'] <=> $b['sort'];
});
$UVLimitType = (int) data_get($link, 'config.wx.uv_limit_type');
$keyPrefix = "link-wx-type{$UVLimitType}-{$link->id}";
$singleUVViews = [];
if ($UVLimitType === UVLimitType::ACCUMULATE->value) {
$key = "{$keyPrefix}-accumulate";
$singleUVViews = Redis::hgetall($key);
} elseif ($UVLimitType === UVLimitType::DAILY->value) {
$ymdDate = date('Ymd');
$key = "{$keyPrefix}-daily-{$ymdDate}";
$singleUVViews = Redis::hgetall($key);
}
// 排除掉已过期的二维码和超出访问上限的二维码
$filteredQrCodes = array_filter($qrCodes, function ($qrCode) use ($singleUVViews) {
$expiredAt = ! empty($expiredAt) ? Carbon::parse($qrCode['expired_at']) : now()->addHour();
$sort = $qrCode['sort'];
$visitUV = $singleUVViews[$sort] ?? 0;
$uvLimitNum = $qrCode['uv_limit_num'] ?? 99999999;
return $expiredAt->greaterThan(now()) && (is_null($uvLimitNum) || $visitUV < $uvLimitNum);
});
if (empty($filteredQrCodes)) {
return [];
}
$filteredQrCodes = array_values($filteredQrCodes);
// 切换方式.
$nextIndex = 0; // 默认返回第一个二维码
$switch_type = data_get($link, 'config.wx.switch_type');
if ($switch_type === SwitchType::SEQUENCE->value) {
// 顺序切换
$lockKey = "link-sequence-{$link->id}";
$indexKey = "link-sequence-index-{$link->id}";
$lock = Cache::lock($lockKey, 1);
if ($lock->get()) {
try {
$currentIndex = Cache::get($indexKey, -1);
$nextIndex = ($currentIndex + 1) % count($filteredQrCodes);
Cache::put($indexKey, $nextIndex);
} finally {
$lock->release();
}
} else {
Log::warning('获取锁失败', ['key' => $lockKey]);
}
} elseif ($switch_type === SwitchType::RANDOM->value) {
// 随机切换
$nextIndex = array_rand($filteredQrCodes);
}
$selectedQrCode = $filteredQrCodes[$nextIndex];
// 更新 Redis 中的访问计数
if ($UVLimitType === UVLimitType::ACCUMULATE->value) {
$key = "{$keyPrefix}-accumulate";
Redis::hincrby($key, $selectedQrCode['sort'], 1);
} elseif ($UVLimitType === UVLimitType::DAILY->value) {
$ymdDate = date('Ymd');
$key = "{$keyPrefix}-daily-{$ymdDate}";
Redis::hincrby($key, $selectedQrCode['sort'], 1);
}
return $selectedQrCode;
}
}
@@ -8,11 +8,25 @@ use App\Enums\UserType;
use App\Enums\UVLimitType;
use App\Models\Domain;
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;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Redis;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Psr16Cache;
use Ugly\Base\Http\Controllers\FormController;
use Ugly\Base\Services\FormService;
@@ -158,10 +172,6 @@ class LinkController extends FormController
$res['config.url'] = 'required|url|starts_with:https://qr61.cn/';
}
if ($iptType === LinkType::QR_QQ->value) {
$res['config.url'] = 'required|url|starts_with:https://ym.link/';
}
return $is_wx ? array_merge($res, [
'config.wx' => [
'required_if:type,'.LinkType::LANDING_MINI->value,
@@ -182,6 +192,283 @@ class LinkController extends FormController
]) : $res;
}
// 获取链接跳转目标
public function target(Request $request, $code): JsonResponse
{
$link = Link::query()->where('code', $code)->first();
if (empty($link)) {
return $this->failed('此链接已被删除!');
}
if ($link->expired_at < now()) {
return $this->failed('链接已过期!');
}
$user = User::query()->findOrFail($link->user_id);
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()
->where('link_id', $link->id)
->where('device_uid', $device_uid)
->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,
'icon' => Storage::url($link->icon),
];
if (
$link->type === LinkType::MINI_PROGRAM ||
$link->type === LinkType::LANDING_MINI
) {
// 跳转到小程序 参数
$mp = MiniProgram::query()->findOrFail(data_get($link, 'config.min_id'));
if (empty($mp) || ! $mp->is_enable) {
return $this->failed('当前小程序不能使用');
}
if ($link->type === LinkType::LANDING_MINI) {
$qr = $this->getWechatNextQr($link);
if (empty($qr)) {
return $this->failed('当前小程序已无可用二维码!');
}
}
$cache['params'] = [
'appid' => $mp->app_id,
'secret' => $mp->secret,
'qr' => $qr ?? null,
'path' => data_get($link, 'config.url') ?: $mp->url,
];
} elseif ($link->type === LinkType::KING_DOC) {
$cache['params']['sid'] = str_replace('https://kdocs.cn/l/', '', data_get($link, 'config.url'));
} elseif ($link->type === LinkType::CLI_QR) {
/**
* 1、电脑端访问草料二维码官网:https://cli.im/img
* 2、上传微信二维码、公众号二维码、企业微信二维码、视频号二维码等等,然后点生成活码
* 3、后台创建草料
*/
$configUrl = str_replace('https://qr61.cn/', '', data_get($link, 'config.url'));
[$userID, $cliID] = explode('/', $configUrl);
$cache['params'] = [
'userID' => $userID,
'cliID' => $cliID,
];
} elseif ($link->type === LinkType::WORK_WECHAT) {
$cache['params'] = [
'url' => data_get($link, 'config.url'),
];
} else {
return $this->failed('暂不支持此链接跳转!');
}
} else {
unset($cache['target']);
}
// 生成链接
if ($link->type === LinkType::WORK_WECHAT) {
$cache['target'] = data_get($cache, 'params.url');
} elseif ($link->type === LinkType::CLI_QR) {
$userID = data_get($cache, 'params.userID');
$cliID = data_get($cache, 'params.cliID');
$getTicketUrl = file_get_contents('https://nc.cli.im/api/weixin/getWxUrlScheme/?query=q=qr61.cn/'.$userID.'/'.$cliID.'&path=pages/code/code&appid=wx5db79bd23a923e8e&org_coding='.$userID);
$fetchUrlData = file_get_contents(data_get(json_decode($getTicketUrl, true), 'data.wx_url_scheme.fetchUrl'));
$arr = json_decode($fetchUrlData, true);
$cache['target'] = data_get($arr, 'data.urlScheme');
} elseif ($link->type === LinkType::KING_DOC) {
$sidUrl = data_get($cache, 'params.sid');
preg_match('/https:\/\/kdocs\.cn\/l\/([a-zA-Z0-9]+)/', $sidUrl, $matches);
$url_link = Http::withUserAgent('Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.99 Safari/537.36')
->get('https://account.kdocs.cn/api/v3/miniprogram/urllink', [
'appid' => 'wx5b97b0686831c076',
'path' => 'pages/navigate/navigate',
'query' => http_build_query([
'url' => 'pages/preview/preview?from=wxminiprogram&fid=256465035925&sid='.$matches[1].'&fname='.urlencode('扫码加微信.docx'),
'scene' => '102',
'jump_from' => 'wechatlogin_guide_passive',
'comp' => 'docx',
'dw' => '1',
]),
'env_version' => 'release',
'is_expire' => 'true',
'expire_time' => time() + 7200,
])->json('url_link');
$jinShanResponse = Http::get($url_link)->throw();
preg_match("/url_scheme:\s*'([^']+)'/", $jinShanResponse, $matches);
$cache['target'] = $matches[1];
} else {
// 小程序
$miniApp = new WechatApp([
'app_id' => data_get($cache, 'params.appid'),
'secret' => data_get($cache, 'params.secret'),
]);
try {
$res = $miniApp->getClient()->postJson('wxa/generatescheme', [
'jump_wxa' => [
'path' => data_get($cache, 'params.path'),
'query' => 'code='.data_get($link, 'code').'&device_uid='.$device_uid,
],
])->toArray();
if (isset($res['errcode']) && $res['errcode'] === 40001) {
$cache = new Psr16Cache(new FilesystemAdapter('easywechat', 1500));
$cache->clear();
}
} catch (\Exception $e) {
$res = [
'errcode' => 1,
'errmsg' => '小程序配置错误',
];
}
if (! empty($res['errcode'])) {
return $this->failed($res['errmsg']);
}
$cache['target'] = data_get($res, 'openlink');
}
if (isset($cache['target']) && $cache['target']) {
LinkVisitLog::query()->create([
'link_id' => $link->id,
'user_id' => $link->user_id,
'ip' => $request->getClientIp(),
'device_uid' => $device_uid,
'cache' => $cache,
]);
unset($cache['params']);
return $this->success($cache);
} else {
return $this->failed('访问错误!');
}
}
// 获取需要展示的二维码
public function getShowQr($code): JsonResponse
{
$link = Link::query()->where('code', $code)->first();
if (empty($link) || ! $link->status) {
return $this->success([
'id' => '',
'avatar' => '',
'title' => '数据不存在~',
'sub_title' => '',
'qr' => '',
]);
}
$config = data_get($link, 'config.wx');
$device_uid = request()->input('device_uid') ?: Str::uuid();
$cache_qr = data_get(LinkVisitLog::query()
->where('link_id', $link->id)
->where('device_uid', $device_uid)
->orderByDesc('id')
->first(), 'cache.params.qr', []);
if (empty($cache_qr)) {
return $this->failed('参数错误!');
}
return $this->success([
'id' => $link->id,
'avatar' => Storage::url(data_get($config, 'avatar')),
'title' => data_get($config, 'title'),
'sub_title' => data_get($config, 'sub_title'),
'qr' => Storage::url(data_get($cache_qr, 'path')),
]);
}
// 获取下一次访问时展示的二维码
private function getWechatNextQr($link)
{
$qrCodes = data_get($link, 'config.wx.qr', []);
// 对链接中的二维码们按照 sort 从小到大排序
usort($qrCodes, function ($a, $b) {
return $a['sort'] <=> $b['sort'];
});
$UVLimitType = (int) data_get($link, 'config.wx.uv_limit_type');
$keyPrefix = "link-wx-type{$UVLimitType}-{$link->id}";
$singleUVViews = [];
if ($UVLimitType === UVLimitType::ACCUMULATE->value) {
$key = "{$keyPrefix}-accumulate";
$singleUVViews = Redis::hgetall($key);
} elseif ($UVLimitType === UVLimitType::DAILY->value) {
$ymdDate = date('Ymd');
$key = "{$keyPrefix}-daily-{$ymdDate}";
$singleUVViews = Redis::hgetall($key);
}
// 排除掉已过期的二维码和超出访问上限的二维码
$filteredQrCodes = array_filter($qrCodes, function ($qrCode) use ($singleUVViews) {
$expiredAt = ! empty($expiredAt) ? Carbon::parse($qrCode['expired_at']) : now()->addHour();
$sort = $qrCode['sort'];
$visitUV = $singleUVViews[$sort] ?? 0;
$uvLimitNum = $qrCode['uv_limit_num'] ?? 99999999;
return $expiredAt->greaterThan(now()) && (is_null($uvLimitNum) || $visitUV < $uvLimitNum);
});
if (empty($filteredQrCodes)) {
return [];
}
$filteredQrCodes = array_values($filteredQrCodes);
// 切换方式.
$nextIndex = 0; // 默认返回第一个二维码
$switch_type = data_get($link, 'config.wx.switch_type');
if ($switch_type === SwitchType::SEQUENCE->value) {
// 顺序切换
$lockKey = "link-sequence-{$link->id}";
$indexKey = "link-sequence-index-{$link->id}";
$lock = Cache::lock($lockKey, 1);
if ($lock->get()) {
try {
$currentIndex = Cache::get($indexKey, -1);
$nextIndex = ($currentIndex + 1) % count($filteredQrCodes);
Cache::put($indexKey, $nextIndex);
} finally {
$lock->release();
}
} else {
Log::warning('获取锁失败', ['key' => $lockKey]);
}
} elseif ($switch_type === SwitchType::RANDOM->value) {
// 随机切换
$nextIndex = array_rand($filteredQrCodes);
}
$selectedQrCode = $filteredQrCodes[$nextIndex];
// 更新 Redis 中的访问计数
if ($UVLimitType === UVLimitType::ACCUMULATE->value) {
$key = "{$keyPrefix}-accumulate";
Redis::hincrby($key, $selectedQrCode['sort'], 1);
} elseif ($UVLimitType === UVLimitType::DAILY->value) {
$ymdDate = date('Ymd');
$key = "{$keyPrefix}-daily-{$ymdDate}";
Redis::hincrby($key, $selectedQrCode['sort'], 1);
}
return $selectedQrCode;
}
// 获取下拉数据源
public function link_list(): JsonResponse
{
+6 -18
View File
@@ -23,29 +23,17 @@ class LoginRequest extends FormRequest
*/
public function rules(): array
{
$rules = [
$code_mode = SystemConfig::get('send_code_mode');
$rule = $code_mode == CodeMode::Email->value ? 'email' : 'regex:/^1[3-9]\d{9}$/';
return [
'username' => [
'required',
'exists:users,username',
'exclude_if:username,admin',
$rule,
],
'password' => 'required|min:6',
];
if (request('username') === 'admin') {
return $rules;
}
$is_open = SystemConfig::get('verify_code_is_open');
if ($is_open) {
$code_mode = SystemConfig::get('send_code_mode');
$rule = $code_mode == CodeMode::Email->value ? 'email' : 'regex:/^1[3-9]\d{9}$/';
$rules['username'] = [
'required',
'exists:users,username',
$rule,
];
}
return $rules;
}
public function messages(): array
+14 -31
View File
@@ -24,50 +24,33 @@ class RegisterRequest extends FormRequest
*/
public function rules(): array
{
$is_open = SystemConfig::get('verify_code_is_open');
if ($is_open) {
$code_mode = SystemConfig::get('send_code_mode');
$rule = ($code_mode == CodeMode::Email->value) ? 'email' : 'regex:/^1[3-9]\d{9}$/';
return [
'username' => [
'required',
$rule,
'unique:users,username',
],
'password' => 'required|min:6|confirmed',
'captcha' => ['required', function (string $attribute, mixed $value, \Closure $fail) {
$test_code = config('services.ali_sms.test_code');
if (empty($test_code) || $value != $test_code) {
$tel = $this->input('username');
$captcha = Cache::get('sms_captcha_'.$tel);
if ((int) $captcha !== (int) $value) {
$fail('验证码错误!');
}
}
}],
'referral_code' => '', // 推荐码
];
}
$code_mode = SystemConfig::get('send_code_mode');
$rule = $code_mode == CodeMode::Email->value ? 'email' : 'regex:/^1[3-9]\d{9}$/';
return [
'username' => [
'required',
$rule,
'unique:users,username',
],
'password' => 'required|min:6|confirmed',
'captcha' => ['required', function (string $attribute, mixed $value, \Closure $fail) {
$test_code = config('services.ali_sms.test_code');
if (empty($test_code) || $value != $test_code) {
$tel = $this->input('username');
$captcha = Cache::get('sms_captcha_'.$tel);
if ((int) $captcha !== (int) $value) {
$fail('验证码错误!');
}
}
}],
'referral_code' => '', // 推荐码
];
}
public function messages(): array
{
$code_mode = SystemConfig::get('send_code_mode');
$is_open = SystemConfig::get('verify_code_is_open');
$txt = '用户名';
if (! empty($is_open)) {
$txt = ($code_mode === CodeMode::Email->value) ? '邮箱' : '手机号';
}
$txt = CodeMode::Email->value ? '邮箱' : '手机号';
return [
'username.required' => "请输入{$txt}",
-9
View File
@@ -4,13 +4,11 @@ namespace App\Jobs;
use App\Jobs\Middleware\RateLimited;
use App\Mail\SendEmail;
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\Facades\Config;
use Illuminate\Support\Facades\Mail;
use Throwable;
@@ -33,13 +31,6 @@ class SendEmailJobs implements ShouldQueue
$this->to = $to;
$this->code = $code;
$this->title = $title ?? '邮箱验证码';
Config::set('mail.mailers.smtp.host', SystemConfig::get('mail_host'));
Config::set('mail.mailers.smtp.port', SystemConfig::get('mail_port'));
Config::set('mail.mailers.smtp.username', SystemConfig::get('mail_username'));
Config::set('mail.mailers.smtp.password', SystemConfig::get('mail_password'));
Config::set('mail.from.address', SystemConfig::get('mail_from_address'));
Config::set('mail.from.name', SystemConfig::get('mail_from_name'));
}
public function middleware()
+1 -16
View File
@@ -2,7 +2,6 @@
namespace App\PayChannels;
use App\Services\SystemConfig;
use EasyWeChat\Pay\Application;
use Illuminate\Support\Carbon;
use Symfony\Component\HttpFoundation\Response;
@@ -18,21 +17,7 @@ class WeChatPayNative
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);
$this->payApp = new Application(config('services.wechat_pay'));
}
// 支付
+1 -7
View File
@@ -17,13 +17,7 @@ class AliDySms
public function __construct()
{
$config = [
'key' => SystemConfig::get('ali_sms_key'),
'secret' => SystemConfig::get('ali_sms_secret'),
'sign_name' => SystemConfig::get('ali_sms_sign_name'),
'test_code' => env('ALI_SMS_TEST_CODE'),
];
$this->config = $config;
$this->config = config('services.ali_sms');
}
/**
+4 -4
View File
@@ -6,7 +6,7 @@
<script>
// window.location = '/web/#/login'
</script>
<title>{!! \App\Services\SystemConfig::get('web_site_title') !!}</title>
<title>火星快链-智慧引流工具</title>
<link rel="stylesheet" href="/css/css.css">
<link rel="stylesheet" href="/css/boot.css">
<style>
@@ -452,7 +452,7 @@
<div class="footer hide-below-1000">
<div class="mainCard d-flex justify-content-between p-4">
<div>
<img class="logo" src="{!! \App\Services\SystemConfig::get('web_site_bottom_logo') !!}">
<img class="logo" src="/image/logo.png">
</div>
<div class="d-flex justify-content-between flex-1">
<div class="line" style="margin-left: 100px"></div>
@@ -471,7 +471,7 @@
<div class="line" style="margin-right: 100px"></div>
</div>
<div>
<img class="weixin" src="{!! \App\Services\SystemConfig::get('web_site_customer_service') !!}">
<img class="weixin" src="/image/web-qr.png">
</div>
</div>
<div class="xlin"></div>
@@ -480,7 +480,7 @@
<div class="show-below-1000 phoneFooter" style="display: none">
<div class="row row-cols-sm-2 p-4">
<div>
<img class="weixin" src="{!! \App\Services\SystemConfig::get('web_site_customer_service') !!}">
<img class="weixin" src="/image/web-qr.png">
<div class="mt-3">
<div class="font-20 text-white">联系我们</div>
<div class="mt-2 text-secondary font-14">公司地址:××××××××××</div>
+2 -3
View File
@@ -7,7 +7,6 @@ use App\Http\Controllers\Api\CommissionController;
use App\Http\Controllers\Api\ConfigController;
use App\Http\Controllers\Api\DomainController;
use App\Http\Controllers\Api\IndexController;
use App\Http\Controllers\Api\JumpController;
use App\Http\Controllers\Api\LinkController;
use App\Http\Controllers\Api\MaterialCateController;
use App\Http\Controllers\Api\MaterialController;
@@ -32,8 +31,8 @@ Route::post('/captcha/sms', [CaptchaController::class, 'sms']); // 获取短信
Route::post('/wechat/payment_notify', [WeChatPayNative::class, 'notify']); // 微信支付回调
// 获取链接重定向目标
Route::get('/link-target/{code}', [JumpController::class, 'target']); // 获取链接跳转地址
Route::get('/link-show-qr/{code}', [JumpController::class, 'getShowQr']); // 显示的二维码信息
Route::get('/link-target/{code}', [LinkController::class, 'target']); // 获取链接跳转地址
Route::get('/link-show-qr/{code}', [LinkController::class, 'getShowQr']); // 显示的二维码信息
// 登录用户都访问
Route::middleware(ApiAuth::class)->group(function (Router $router) {
+3
View File
@@ -1,5 +1,8 @@
<?php
use Illuminate\Foundation\Inspiring;
use Illuminate\Support\Facades\Artisan;
/*
|--------------------------------------------------------------------------
| Console Routes
-6
View File
@@ -3,18 +3,12 @@
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');
}
public function test_get_token()
{
dd($this->get_admin_token());