普通视图

发现新文章,点击刷新页面。
昨天以前首页

微信闪照小程序实现

作者 幸运是我
2025年8月18日 16:32

已经有一年半没有写文章了,今天给掘友们写一个闪照实现的demo,纯前端开发技术栈为uniapp+uni云开发;先贴出代码

首先是闪照的几个要点(小程序申请注册啥的就不说了,只说功能)

  1. 上传图片到uni云存储空间
  2. 上传图片需要做违规检测
  3. 闪照需要分享出去,微信分享功能
  4. 查看闪照时需要限时查看和防止手机截屏

image.png

image.png

<view wx:if="{{!isBlackScreen}}" class="page-container {{isBlackScreen ? 'black-screen' : ''}} container">
<view class="upload-area">
<up-upload :fileList="fileList" @afterRead="afterRead" @delete="deletePic" name="1" multiple :maxCount="1"
width="400" height="500">
</up-upload>
</view>
<!-- 按钮区域 -->
<view class="button-group">
<!-- 隐藏的上传组件 -->
<up-upload ref="uploadRef" :fileList="fileList" @afterRead="afterRead" @delete="deletePic" name="1" multiple
:maxCount="1" style="display: none;"></up-upload>
<u-button class="action-button" shape="circle" icon="photo" text="选择照片" @click="handleSelectPhoto" />
<u-button class="action-button share-button" shape="circle" icon="share" text="分享" open-type="share" :disabled="!canShare" />
</view>
<custom-tabbar :current="currentTab"></custom-tabbar>
</view>
<view wx:if="{{isBlackScreen}}" class="black-screen-overlay">
<text>禁止截图或录屏</text>
</view>
</template>

<script setup>
import {
ref
} from 'vue'
import {
onLoad,
onShow,
onNavigationBarButtonTap,
onPullDownRefresh,
onReachBottom,
onUnload,
onShareAppMessage
} from '@dcloudio/uni-app';
import CustomTabbar from '../components/custom-tabber.vue'
const currentTab = ref(0) //tabbar
const fileList = ref([]);
const subscribeNotify = ref(false);
const allowForward = ref(false);
const uploadRef = ref(null);
const canShare = ref(false); // 新增:控制是否允许分享
const handleSelectPhoto = () => {
// 手动触发上传组件的选择文件
uploadRef.value?.chooseFile();
};
const isBlackScreen = ref(false) // 是否显示黑屏
onLoad(() => {
wx.showShareMenu({
menus: ['shareAppMessage', 'shareTimeline'],
success() {
console.log('分享功能已启用')
}
})
wx.onUserCaptureScreen(() => {
this.setData({
isBlackScreen: true
}); // 触发黑屏

// 3秒后恢复(可选)
setTimeout(() => {
this.setData({
isBlackScreen: false
});
}, 3000);
});

})
onLoad(() => {

})
onUnload(() => {
wx.offUserCaptureScreen(); // 移除监听
});
onShareAppMessage(() => {
if (!canShare.value || !fileID.value) {
uni.showToast({
title: '请先上传图片',
icon: 'none'
});
return {};
}
console.log(fileID.value); // 查看 fileID 是否正常
return {
title: '查看闪照',
path: '/pages/viewImg/viewImg?fileID=' + fileID.value, // 带参数的分享路径
imageUrl: '/static/sz.png', // 分享图片
success(res) {
uni.showToast({
title: '分享成功'
})
},
fail(err) {
console.log('分享失败', err)
}
}
})
// 删除图片
const deletePic = (event) => {
fileList.value.splice(event.index, 1);
canShare.value = false; // 删除图片后禁止分享
};
const toview = () => {
uni.navigateTo({
url: '/pages/viewImg/viewImg?fileID=' + fileID.value, // 带参数的分享路径
})
}
const handleToTop = () => {
uni.navigateTo({
url: '/pages/wgbtop/wgbtop',
})
}
const afterRead = async (event) => {
fileList.value = []
canShare.value = false; // 开始上传时先禁止分享
let lists = [].concat(event.file);
console.log('选择的文件:', lists);
let fileListLen = fileList.value.length;

// 更新UI状态
lists.map((item) => {
fileList.value.push({
...item,
status: 'checking',
message: '安全检测中',
});
});

// 显示加载中状态
uni.showLoading({
title: '正在加载中...',
mask: true
});

// 读取文件的辅助函数
const readFileContent = async (fileItem) => {
// H5环境
if (fileItem.file && fileItem.file instanceof File) {
return await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => resolve(e.target.result);
reader.onerror = reject;
reader.readAsArrayBuffer(fileItem.file);
});
}
// 小程序环境
else if (fileItem.url) {
return await new Promise((resolve, reject) => {
uni.getFileSystemManager().readFile({
filePath: fileItem.url,
encoding: 'binary',
success: res => resolve(res.data),
fail: reject
});
});
}
throw new Error('不支持的文件类型');
};

for (let i = 0; i < lists.length; i++) {
let uploadResult = null;
try {
// 更新状态为上传中
fileList.value[i].status = 'uploading';
fileList.value[i].message = '正在加载...';

// 读取文件内容
const fileContent = await readFileContent(lists[i]);

// 更新状态为检测中
fileList.value[i].status = 'checking';
fileList.value[i].message = '安全检测中...';
// 调用云函数进行安全检测
const checkResult = await uniCloud.callFunction({
name: 'imgSecCheck',
data: {
fileContent: fileContent
}
});
if (checkResult.result.code !== 0) {
throw new Error(checkResult.result.message || '图片安全检测未通过');
}
console.log('安全检测通过', checkResult);
// 更新状态为上传中
fileList.value[i].status = 'uploading';
// fileList.value[i].message = '正在上传...';
// 安全检测通过后再上传到uniCloud
uploadResult = await uploadToUniCloud(lists[i]);

let item = fileList.value[fileListLen];
fileList.value.splice(fileListLen, 1, {
...item,
status: 'success',
message: '加载成功',
url: uploadResult.fileID,
});
fileListLen++;

// 上传成功,允许分享
canShare.value = true;

// 隐藏加载中
uni.hideLoading();
uni.showToast({
title: '加载成功',
icon: 'success',
duration: 2000
});
} catch (error) {
console.error('检测或上传失败:', error);
let item = fileList.value[fileListLen];

let message = '上传失败,图片可能包含违规内容';
if (error.message && error.message.includes('违规')) {
message = '图片包含违规内容';
} else if (error.message && error.message.includes('大小')) {
message = '图片大小超过限制(10MB)';
} else if (error.errMsg && error.errMsg.includes('fail')) {
message = '安全检测服务异常';
}
fileList.value.splice(fileListLen, 1, {
...item,
status: 'failed',
message: message,
});
fileListLen++;
// 上传失败,禁止分享
canShare.value = false;
// 隐藏加载中并显示错误
uni.hideLoading();
uni.showToast({
title: message,
icon: 'none',
duration: 3000
});
// 如果上传了文件但检测失败,删除已上传的文件
if (uploadResult && uploadResult.fileID) {
try {
await uniCloud.deleteFile({
fileList: [uploadResult.fileID]
});
console.log('已删除未通过检测的文件');
} catch (deleteError) {
console.error('删除文件失败:', deleteError);
}
}
}
}
};
// 上传到uniCloud云存储
const fileID = ref()
const uploadToUniCloud = async (fileItem) => {
// 如果是H5环境且有原始File对象
if (fileItem.file && process.env.VUE_APP_PLATFORM === 'h5') {
// H5方式上传
const cloudPath = 'uploads/' + Date.now() + '-' + fileItem.file.name + '.png';
const res = await uniCloud.uploadFile({
filePath: fileItem.file,
cloudPath: cloudPath
});
fileID.value = res.fileID
return res;
} else {
// 小程序/APP方式上传
const cloudPath = 'uploads/' + Date.now() + '-' + Math.random().toString(36).substring(2) + '.png';
const res = await uniCloud.uploadFile({
filePath: fileItem.url,
cloudPath: cloudPath
});
fileID.value = res.fileID
console.log(fileID.value)
return res;
}
};
onShow(() => {
uni.hideTabBar()
// 根据当前页面设置currentTab
const pages = getCurrentPages()
const page = pages[pages.length - 1]
const route = page.route
if (route === 'pages/index/index') {
currentTab.value = 0
} else if (route === 'pages/wgbtop/wgbtop') {
currentTab.value = 1
} else if (route === 'pages/user/user') {
currentTab.value = 2
}
})
</script>

<style lang="scss" scoped>
.container {
// padding: 24rpx;
padding: 20rpx;
box-sizing: border-box;
background-color: #f8f8f8;
min-height: 100vh;
}

.black-screen-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: black;
color: white;
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}

.upload-area {
height: 1000rpx;
// background-color: #fff;
border-radius: 16rpx;
margin-bottom: 32rpx;
display: flex;
align-items: center;
justify-content: center;
// box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
}

.button-group {
display: flex;
justify-content: space-between;
margin-bottom: 32rpx;

.action-button {
flex: 1;
height: 80rpx;
font-size: 28rpx;
background: linear-gradient(135deg, #f5f7fa 0%, #e4e8eb 100%);
border: none;
color: #333;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);

&:active {
opacity: 0.9;
}

&.share-button {
margin-left: 24rpx;
background: linear-gradient(135deg, #3c9cff 0%, #2b85e4 100%);
color: #fff;

&.u-button--disabled {
opacity: 0.6;
}
}
}
}

.settings-section {
background-color: #fff;
border-radius: 16rpx;
overflow: hidden;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);

:deep(.u-cell) {
padding: 28rpx 32rpx;
}

:deep(.u-cell_title) {
font-size: 30rpx;
color: #333;
font-weight: 500;
}
}
</style>
1.上传图片到uni云存储空间

首先上传图片用两个地方,一个是上传组件,一个是点击上传的按钮,所以我写了两个up-upload组件,一个是显示的,一个是隐藏的,隐藏的组件用于实现按钮点击上传,使用uploadRef.value?.chooseFile();来手动触发;我使用的是uni的云存储方法为uniCloud.uploadFile(),需要这个uniapp账号开通了云存储空间,可以免费开通看不懂的话可以点击这段去uni的云存储板块看教程

2.上传图片需要做违规检测

第二点就是上传时需要做违规检测,如果用户上传了色情恐怖等等就不让检测上传分享了,这一块微信有提供检测的api————api.weixin.qq.com/wxa/img_sec… ;检测的api有两个一个只需要token就可以了;我使用的就是这个。另一个需要用户的openid,由于我没有做登录所以要openid的我就没有使用,这一块主要是获取token去调用这个检测接口,我使用的是uni的云函数代码如下

exports.main = async (event, context) => {
  // 获取微信access_token
  const getAccessToken = async () => {
    const res = await uniCloud.httpclient.request(
      'https://api.weixin.qq.com/cgi-bin/token', 
      {
        method: 'GET',
        data: {
          grant_type: 'client_credential',
          appid: 替换为你的小程序AppID
          secret:替换为你的小程序AppSecret
        },
        dataType: 'json'
      }
    )
    return res.data.access_token
  }

  try {
    const access_token = await getAccessToken()
    
    // 阿里云不支持downloadFile,直接从event中获取文件内容
    const fileContent = event.fileContent
    
    // 调用微信安全检测接口
    const result = await uniCloud.httpclient.request(
      `https://api.weixin.qq.com/wxa/img_sec_check?access_token=${access_token}`,
      {
        method: 'POST',
        content: fileContent,
        headers: {
          'Content-Type': 'application/octet-stream'
        },
        dataType: 'json'
      }
    )
    
    if (result.data.errcode === 0) {
      return {
        code: 0,
        message: '检测成功',
        data: result.data
      }
    } else {
      return {
        code: result.data.errcode || -1,
        message: result.data.errmsg || '检测失败',
        data: result.data
      }
    }
  } catch (error) {
    return {
      code: -2,
      message: error.message || '检测异常',
      data: error
    }
  }
}
3.闪照需要分享出去,微信分享功能

微信的分享功能这一块没有啥好说的很简单,给按钮加上open-type="share",然后吧分享功能打开通过onShareAppMessage方法就可以分享了 主要代码如下

<u-button class="action-button share-button" shape="circle" icon="share" text="分享" open-type="share" :disabled="!canShare" />


wx.showShareMenu({
menus: ['shareAppMessage', 'shareTimeline'],
success() {
console.log('分享功能已启用')
}
})
                
                onShareAppMessage(() => {
if (!canShare.value || !fileID.value) {
uni.showToast({
title: '请先上传图片',
icon: 'none'
});
return {};
}
return {
title: '查看闪照',
path: '/pages/viewImg/viewImg?fileID=' + fileID.value, // 带参数的分享径
imageUrl: '/static/sz.png', // 分享图片
success(res) {
uni.showToast({
title: '分享成功'
})
},
fail(err) {
console.log('分享失败', err)
}
}
})
4.查看闪照时需要限时查看和防止手机截屏

第四点主要是通过css模糊效果结合定时器来实现;判断是否看过的字段我存储在了本地存储中,防君子不防小人。防截屏使用的是微信提供的wx.setVisualEffectOnCapture方法;具体代码如下

<template>
<view class="image-container">
<!-- 使用两层图片结构,一层模糊层,一层清晰层 -->
<image v-if="isBlurred" :src="imageSrc" mode="widthFix" class="blur-layer" 
@touchstart="handleTouchStart" @touchend="handleTouchEnd" @touchcancel="handleTouchEnd" />
<image :src="imageSrc" mode="widthFix" :class="['sharp-layer', { 'visible': !isBlurred }]" 
@touchstart="handleTouchStart" @touchend="handleTouchEnd" @touchcancel="handleTouchEnd" />
<view v-if="hasViewed" class="hint-text">
<up-button :plain="true" class="" style="margin-top: 40rpx;width: 180rpx;" size='mini'
@click="toIndex">我也要发照片</up-button>
</view>
<up-modal :show="show" :title="title" :content='content' @confirm="confirm" :closeOnClickOverlay="true"
showCancelButton='true' @cancel='cancel'></up-modal>
<view v-if="showBlackScreen" class="black-screen">
<text class="hint-text">禁止截屏</text>
</view>
</view>
</template>

<script setup>
import {
ref
} from 'vue'
import {
onLoad,
onShow,
onNavigationBarButtonTap,
onPullDownRefresh,
onUnload,
onHide
} from '@dcloudio/uni-app';
import {
onUnmounted
} from 'vue';
const imageSrc = ref(
'https://mp-57911374-353d-4222-b8c2-1a8948d61be7.cdn.bspapp.com/cloudstorage/4e16e15d-6660-4c24-af36-d6886d1e3a7e.'
)
const isBlurred = ref(true)
const hasViewed = ref(false) // 是否已经查看过
let timer = null
const show = ref(false);
const title = ref('提示');
const content = ref('您已经查看过该图片');
const imgArray = ref([])

onLoad((options) => {
if (uni.getStorageSync('imgArray')) {
imgArray.value = uni.getStorageSync('imgArray')
}
if (options) {
imageSrc.value = options.fileID
const isExist = imgArray.value.some(item => item === imageSrc.value);
if (isExist) {
hasViewed.value = true
isBlurred.value = true // 修改这里:已经查看过的图片保持模糊状态
} else {
hasViewed.value = false
isBlurred.value = true
}
}
wx.setVisualEffectOnCapture({
visualEffect: 'hidden',
});
})

onHide(() => {
wx.setVisualEffectOnCapture({
visualEffect: 'none',
});
})

onUnload(() => {
wx.setVisualEffectOnCapture({
visualEffect: 'none',
});
})

const handleTouchStart = () => {
// 已经查看过,直接显示提示
if (hasViewed.value) {
show.value = true
return
}

// 清除之前的定时器
clearTimeout(timer)
// 立即显示清晰图片
isBlurred.value = false

// 设置2秒后自动恢复模糊
timer = setTimeout(() => {
isBlurred.value = true
hasViewed.value = true // 标记为已查看
imgArray.value.push(imageSrc.value)
uni.setStorageSync('imgArray', imgArray.value); //存本地
}, 2000)
}

const handleTouchEnd = () => {
// 已经查看过的不处理
if (hasViewed.value) return
// 如果触摸时间不足2秒就松手,也恢复模糊并标记为已查看
clearTimeout(timer)
isBlurred.value = true
hasViewed.value = true
imgArray.value.push(imageSrc.value)
uni.setStorageSync('imgArray', imgArray.value);
}

// 去看广告
const confirm = () => {
show.value = false
};
// 不看
const cancel = () => {
show.value = false
};

onShow(() => {
wx.setVisualEffectOnCapture({
visualEffect: 'hidden',
});
})

const toIndex = () => {
uni.switchTab({
url: '/pages/index/index'
})
}

onUnmounted(() => {
clearTimeout(timer)
});
</script>

<style scoped>
/* 容器确保图片比例不变形 */
.image-container {
width: 100%;
height: 80vh;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}

.black-screen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
}

/* 模糊层 */
.blur-layer {
width: 100%;
display: block;
position: absolute;
filter: blur(22px);
transform: scale(1.02);
transition: opacity 0.5s ease;
}

/* 清晰层 */
.sharp-layer {
width: 100%;
display: block;
position: absolute;
opacity: 0;
transition: opacity 0.5s ease;
}

.sharp-layer.visible {
opacity: 1;
}

.hint-text {
position: absolute;
bottom: 50%;
left: 0;
right: 0;
text-align: center;
color: white;
padding: 10rpx 20rpx;
border-radius: 10rpx;
margin: 0 auto;
width: max-content;
z-index: 10;
}

/* 性能优化 */
@media (prefers-reduced-motion: reduce) {
.blur-layer, .sharp-layer {
transition: none;
}
}
</style>

到这里整个功能就已经实现完了,主要两个页面一个是上传图片和分享的页面;一个是查看闪照的页面。整篇文章都是干货无划水;喜欢的朋友可以点赞收藏一下,感谢了。下一篇我会分享纯前端实现的情侣互动点餐小程序。

拯救你的app/小程序审核!一套完美避开审核封禁的URL黑名单机制

2025年8月17日 18:25

app/微信小程序审核又双叒叕被拒了?因为一个历史遗留页面导致整个小程序被封禁搜索?别让精心开发的app/小程序毁在几个不起眼的URL上!本文将揭秘我们在多次惨痛教训后总结出的终极解决方案。

前言:每个小程序开发者都经历过的噩梦

凌晨两点,微信审核通知:"您的小程序因存在违规页面,搜索功能已被限制"。看着辛苦运营的用户量断崖式下跌,排查三天才发现是因为一个早已下架但还能访问的历史页面。这不是假设,而是真实发生的灾难场景

在经历多次微信审核失败后,我们意识到:必须有一套灵活、实时的URL黑名单机制,能够在app/微信审核发现问题前,快速屏蔽任何违规页面。这套系统需要:

  1. 分钟级响应:新发现的违规URL,1分钟内全局生效

  2. 精准打击:既能拦截整个页面,也能封禁特定参数组合

  3. 零误杀:确保正常页面不受影响

  4. 优雅降级:被拦截用户跳转到友好提示页,且可一对一设置兜底页。

下面是我们用血泪教训换来的完整解决方案,已成功帮助我们通过n多次app审核。

app/微信小程序审核的致命陷阱:你未必意识到的风险点

真实审核失败案例

  • 案例1:三年前的活动页仍可通过直接URL访问(违反现行规则)

  • 案例2:用户生成内容包含敏感关键词(UGC页面)

  • 案例3:第三方合作伙伴的H5页面突然变更内容

  • 最致命案例:历史页面被微信爬虫索引,导致整个小程序搜索功能被封禁

核心需求清单(微信审核视角)

  1. 实时封堵能力:无需发版即可封禁任意URL

  2. 精准匹配:支持完整URL和带参数的URL匹配

  3. 全类型覆盖:原生页面 + H5页面统一处理

  4. 优雅降级:被封禁用户看到友好提示而非404

  5. 安全兜底:系统异常时自动放行,不影响正常业务

系统架构设计:三重防护盾

核心流程

  1. 所有跳转请求经过黑名单检查

  2. 命中规则则跳转到兜底页

  3. 系统异常时降级放行

  4. 后台配置秒级生效

核心技术实现

参数级精准打击 - 只封禁违规内容

// 黑名单配置
["pages/user/content?type=sensitive"]

// 结果:
"pages/user/content?type=normal" => 放行 ✅
"pages/user/content?type=sensitive" => 拦截 ⛔

微信审核场景:当只有特定参数组合违规时,最小化业务影响

匹配规则详解:如何应对app审核

场景1:紧急封禁整个页面(后台配置示例)

{
  "YourBlackList": [
    {
      "nowUrl": "https://baidu.com",
      "ToUrl": "www.juejin.cn"
    }
  ]
}

只要命中 baidu.com 无论实际跳转页面后面参数是什么,都命中了黑名单,直接跳转到自己的兜底页](url)

场景2:精准封禁违规内容

// 配置黑名单
{
  "YourBlackList": [
    {
      "nowUrl": "pages/news/detail?id=12345",
      "ToUrl": "www.baidu.com"
    }
  ]
}
// 效果:
仅拦截id=12345的新闻,如果命中,则跳转到百度(你设置的兜底页)。其他正常展示

场景3:批量处理历史内容

// 配置黑名单
{
  "YourBlackList": [
    {
      "nowUrl": "pages/history/?year=2020",
      "ToUrl": "www.baidu.com"
    }
  ]
}

// 效果:
拦截2020年的所有历史页面,其他年份正常

实际应用:拯救审核的最后一公里

在路由跳转处拦截

async function myNavigateTo(url) {
  const { isBlocked, ToUrl } = checkUrlBlacklist(url);
  if (isBlocked) {
    console.warn('审核风险页面被拦截:', url);
    // 跳转到安全页
    return wx.navigateTo({ url: ToUrl });
  }
  
  // 正常跳转逻辑...
}

性能与安全:双保险设计

二重保障机制

  1. 性能优化:黑名单为空时短路返回
if (!blackUrlList.length) return { isBlocked: false };
  1. 频率控制:避免相同URL重复解析

更新时机

app/小程序初始化时,如果想更精细一些,可以监听app/小程序后台切到前台onShow时

  // 获取阿波罗接口配置
      const resp = await request({
        url: 'https://你的后台配置接口',
      });
      // 这里blackUrlInfoList需要保存在全局,可以放在本地存储下
      blackUrlInfoList = res.blackUrlInfoList || []

校验时机:每次跳转时。

具体判断逻辑在此不做阐述,

总结:从此告别审核噩梦

通过实施这套URL黑名单系统,我们实现了:

  • 审核通过率从63% → 98%

  • 问题响应时间从2天 → 5分钟

  • 搜索封禁事故0发生

关键收获

  1. 提前拦截比事后补救更重要

  2. 参数级控制最大化保留正常功能

  3. 实时配置能力是应对审核的关键

现在点击右上角收藏本文,当app审核再次亮红灯时,你会感谢今天的自己!


分享你的审核故事:你在微信/app审核中踩过哪些坑?欢迎在评论区分享你的经历和解决方案!

❌
❌