This commit is contained in:
@jyuhsin
2026-01-04 14:28:56 +08:00
parent d2cdc72f3c
commit 1f5f221850
8 changed files with 534 additions and 2 deletions
@@ -9,6 +9,7 @@ import com.budwk.app.web.commons.auth.satoken.SaTokenDaoRedisImpl;
import com.budwk.app.web.commons.auth.satoken.StpInterfaceImpl;
import com.budwk.app.web.commons.base.Globals;
import com.budwk.app.web.commons.ext.pubsub.WebPubSub;
import com.budwk.app.zhgh.scan.core.ScanDispatcher;
import lombok.extern.slf4j.Slf4j;
import org.beetl.core.GroupTemplate;
import org.nutz.boot.NbApp;
@@ -81,7 +82,9 @@ public class MainLauncher {
init_task();
init_auth();
ioc.get(Globals.class);
}
ioc.get(ScanDispatcher.class);
}
public void init_auth() {
SaTokenConfig saTokenConfig = conf.makeDeep(SaTokenConfig.class, PRE);
@@ -0,0 +1,91 @@
package com.budwk.app.zhgh.activity.culture.scanHandle;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.result.Result;
import com.budwk.app.sys.views.View_user;
import com.budwk.app.web.commons.auth.utils.SecurityUtil;
import com.budwk.app.zhgh.activity.basic.models.ActivityUserScope;
import com.budwk.app.zhgh.activity.culture.models.ActivityTissue;
import com.budwk.app.zhgh.activity.culture.models.ActivityTissuePerson;
import com.budwk.app.zhgh.scan.core.ScanHandler;
import org.nutz.dao.Cnd;
import org.nutz.dao.Dao;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.Map;
/**
* @ClassName CultureScanHandle
* @Author JyuHsin
* @Date 2025/12/30 15:43
* @Version 1.0
* @Description TODO
*/
@IocBean
public class CultureScanHandle implements ScanHandler {
@Inject
private Dao dao;
@Override
public String getBizType() {
return "culture";
}
@Override
public Object handle(Map<String, Object> params) {
// 获取参数
String userId = params.get("userId").toString();
boolean handle = (Boolean) params.get("handle");
String activityId = "123";
View_user user = dao.fetch(View_user.class, Cnd.where("id", "=", userId));
// 查询活动
ActivityTissue tissue = dao.fetch(ActivityTissue.class, activityId);
if (tissue == null || tissue.getGroupId() == null) {
return Result.error(99, user.getUsername() + "不在活动范围内");
}
int scopeCount = dao.count(ActivityUserScope.class, Cnd.where("groupId", "=", tissue.getGroupId()).and("userId", "=", userId));
if (scopeCount == 0) {
return Result.error(99, user.getUsername() + "不在活动范围内");
}
ActivityTissuePerson tissuePerson = dao.fetch(ActivityTissuePerson.class, Cnd.where("tissueId", "=", activityId).and("userId", "=", userId));
if (tissuePerson != null) {
return Result.error(99, user.getUsername() + "已入场<br/>入场时间:" + tissuePerson.getApplyDateTime());
}
// 插入报名表
if(handle) {
ActivityTissuePerson person = new ActivityTissuePerson();
person.setTissueId(activityId);
person.setUserId(userId);
person.setApplyUserId(SecurityUtil.getUserId());
person.setApplyUserUserName(SecurityUtil.getUserUsername());
person.setUserName(user.getUsername());
person.setLoginName(user.getLoginname());
person.setSex(user.getSex());
person.setMobile(user.getMobile());
person.setUnitName(user.getUnitName());
person.setUnionName(user.getUnionName());
person.setSign(false);
person.setApplyDateTime(DateUtil.now());
person.setUnitId(user.getUnitId());
person.setUnionId(user.getUnionId());
dao.insert(person);
return Result.success();
}
String msg = "姓名:" + user.getUsername() + "<br/>";
msg += "工号:" + user.getLoginname() + "<br/>";
msg += "所属单位:" + user.getUnitName() + "<br/>";
msg += "所属工会:" + user.getUnionName() + "<br/>";
msg += "身份证号:" + StrUtil.blankToDefault(user.getIdCard(), "暂无") + "<br/>";
return Result.success(msg);
}
}
@@ -0,0 +1,52 @@
package com.budwk.app.zhgh.scan.controller;
import cn.dev33.satoken.annotation.SaCheckLogin;
import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.hutool.core.util.StrUtil;
import com.budwk.app.base.result.Result;
import com.budwk.app.zhgh.scan.core.ScanDispatcher;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import org.nutz.json.Json;
import org.nutz.mvc.annotation.At;
import org.nutz.mvc.annotation.Ok;
import org.nutz.mvc.annotation.POST;
import org.nutz.mvc.annotation.Param;
import java.util.Map;
/**
* @ClassName ScanController
* @Author JyuHsin
* @Date 2025/12/29 15:28
* @Version 1.0
* @Description TODO
*/
@Slf4j
@IocBean
@Ok("json:full")
@Api(tags = "活动扫码")
@At("/platform/scan")
public class ScanController {
@Inject
private ScanDispatcher dispatcher;
@At("/h5")
@SaCheckPermission("h5.scan")
@Ok("beetl:/platform/zhghh5/scan/index.html")
public void h5Index() {}
@At("/handle")
@Ok("json")
@SaCheckLogin
public Object handle(@Param("bizType") String bizType, @Param("params") String params) {
if(StrUtil.isBlank(bizType)) {
return Result.error("参数缺失");
}
Map<String, Object> paramsMap = Json.fromJson(Map.class, params);
return dispatcher.dispatch(bizType, paramsMap);
}
}
@@ -0,0 +1,52 @@
package com.budwk.app.zhgh.scan.core;
import org.nutz.ioc.Ioc;
import org.nutz.ioc.loader.annotation.Inject;
import org.nutz.ioc.loader.annotation.IocBean;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @ClassName ScanDispatcher
* @Author JyuHsin
* @Date 2025/12/30 15:10
* @Version 1.0
* @Description TODO
*/
@IocBean(create = "init")
public class ScanDispatcher {
@Inject("refer:$ioc")
private Ioc ioc;
// NutzBoot 自动注入所有 @IocBean 的 ScanHandler 实例到 Map
@Inject
private Map<String, ScanHandler> handlerBeans;
private Map<String, ScanHandler> bizTypeToHandler = new HashMap<>();
public ScanDispatcher() {}
public void init() {
String[] names = ioc.getNamesByType(ScanHandler.class);
System.out.println("找到 ScanHandler Bean 名称: " + names);
for (String name : names) {
ScanHandler handler = ioc.get(ScanHandler.class, name);
bizTypeToHandler.put(handler.getBizType(), handler);
}
System.out.println("已加载 ScanHandler: " + bizTypeToHandler.keySet());
}
public Object dispatch(String bizType, Map<String, Object> params) {
ScanHandler handler = bizTypeToHandler.get(bizType);
if (handler == null) {
throw new IllegalArgumentException("不支持的业务类型: " + bizType);
}
return handler.handle(params);
}
}
@@ -0,0 +1,22 @@
package com.budwk.app.zhgh.scan.core;
import java.util.Map;
/**
* @ClassName ScanHandler
* @Author JyuHsin
* @Date 2025/12/30 15:09
* @Version 1.0
* @Description TODO
*/
public interface ScanHandler {
String getBizType(); // 如 "meeting"
/**
* 直接传参,不包装 Context
* @param params 业务参数
* @return 处理结果
*/
Object handle(Map<String, Object> params);
}
@@ -67,6 +67,9 @@
<!--g2plot-->
<script src="${base!}/assets/platform/plugins/g2plot/g2plot.min.js"></script>
<!--二维码-->
<script src="${base!}/assets/platform/plugins/vue-qrcode/index.js"></script>
<script>
window._AMapSecurityConfig = {
securityJsCode: "4fa1e1aeabba7eb9518129cf57ab17c1"
@@ -262,6 +265,7 @@
</script>
<script>
Vue.component(VueQrcode.name, VueQrcode)
Vue.component("rich-text", httpVueLoader("/components/plugins/sysRichTextView/index.vue?v=" + new Date().getTime()))
Vue.component("h5-file-upload", httpVueLoader("/components/plugins/sysUpload/h5Index.vue?v=" + new Date().getTime()))
Vue.component("h5-signature", httpVueLoader("/components/plugins/sysSignature/h5Index.vue?v=" + new Date().getTime()))
@@ -0,0 +1,272 @@
<!--#
layout("/layouts/platform_h5.html"){
#-->
<link rel="stylesheet" href="https://res.wx.qq.com/open/libs/weui/2.2.0/weui.min.css">
<style>
#app {
min-height: 100vh;
background: #f7f8fa;
}
.scan-container {
width: 100%;
height: 65vh;
position: relative;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
}
/* 扫描框 - 更大、圆角、柔和边框 */
.scan-box {
position: absolute;
width: 260px;
height: 260px;
border: 2px solid #1989fa;
border-radius: 16px;
z-index: 2;
}
/* 扫描线 - 渐变色 + 柔和动画 */
.scan-line {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 3px;
background: linear-gradient(to right, transparent, #1989fa, transparent);
animation: scan 2.5s ease-in-out infinite;
z-index: 3;
}
@keyframes scan {
0% { top: 0; opacity: 0.8; }
50% { opacity: 1; }
100% { top: 257px; opacity: 0.8; }
}
.footer-container {
position: fixed;
bottom: 16px;
width: 100%;
}
.footer-container .van-button--block {
width: 90%;
margin: 0 auto 6px;
height: 50px;
border-radius: 12px;
}
#reader {
width: 90%;
height: 60vh;
margin: 10px auto 40px;
text-align: center;
position: relative;
overflow: hidden;
}
#reader video {
width: 100% !important;
height: 100% !important;
object-fit: cover !important;
position: absolute !important;
top: 0 !important;
left: 0 !important;
}
.overlay {
position: absolute;
top: 0; left: 0;
width: 100%; height: 65vh;
pointer-events: none;
z-index: 10;
}
.weui-msg__title {
font-weight: normal;
}
.weui-msg__msg {
font-weight: bolder;
}
.weui-msg {
padding-top: 20px;
}
.weui-msg__icon-area {
margin-bottom: 20px;
}
</style>
<div id="app" v-cloak>
<van-sticky>
<van-nav-bar title="扫一扫" left-text="返回" left-arrow @click-left="historyBack" fixed placeholder></van-nav-bar>
</van-sticky>
<div v-if="!scanStatus" class="scan-container overlay">
<!-- 扫描框 -->
<div class="scan-box">
<div class="scan-line"></div>
</div>
<!-- 初始图标(仅在未启动时显示) -->
<van-icon v-if="!scanStatus" name="scan" size="56" color="#1989fa" style="z-index: 2;"></van-icon>
</div>
<div id="reader"></div>
<div class="footer-container">
<van-button v-if="scanStatus" @click="closeScan" block color="#006DB9">关闭扫描</van-button>
<van-button v-else @click="initCamera" block color="#006DB9">点击扫描</van-button>
</div>
<van-dialog v-model="resultVisible"
@confirm="resultConfirm"
@cancel="isProcessing = false; initCamera()"
:confirm-button-text="result?.code === 0 ? '确认入场' : '确认'"
:show-cancel-button="result?.code === 0"
cancel-button-text="取消入场"
>
<div class="weui-msg">
<div class="weui-msg__icon-area">
<i v-if="result?.code === 0" class="weui-icon-success weui-icon_msg"></i>
<i v-else class="weui-icon-warn weui-icon_msg"></i>
</div>
<div class="weui-msg__text-area">
<div class="weui-msg__title">查询结果</div>
<div class="weui-msg__msg" v-html="result?.msg"></div>
</div>
</div>
</van-dialog>
</div>
<script src="${base!}/assets/platform/plugins/html5-qrcode/html5-qrcode.min.js"></script>
<script>
new Vue({
el: '#app',
data() {
return {
scanStatus: false,
html5QrCode: null,
isProcessing: false, // 防止重复处理
resultVisible: false,
result: null,
decodedText: null,
}
},
methods: {
async initCamera() {
try {
const devices = await Html5Qrcode.getCameras()
if (!devices || devices.length === 0) {
vant.Dialog.alert({
title: '提示',
message: '未检测到摄像头,请检查设备权限或尝试其他浏览器'
}).then(() => {})
return;
}
let cameraId
if (devices.length > 1) {
cameraId = devices[1].id;
} else {
cameraId = devices[0].id;
}
let isHuawei = navigator.userAgent.toLowerCase().match(/huawei/i) == 'huawei';
const backCamera = devices.find(d => /back|rear/i.test(d.label));
if (isHuawei && backCamera) cameraId = backCamera.id;
await this.startScanning(cameraId);
} catch (err) {
console.error('摄像头初始化失败:', err);
let msg = '请允许访问摄像头权限';
if (err.name !== 'NotAllowedError') {
msg = '摄像头启动失败,请重试或更换设备';
}
vant.Dialog.alert({
title: '权限提示',
message: msg
}).then(() => {})
}
},
async startScanning(cameraId) {
this.html5QrCode = new Html5Qrcode("reader");
this.scanStatus = true;
try {
await this.html5QrCode.start(
cameraId,
{
fps: 300,
qrbox: { width: 300, height: 300 },
aspectRatio: 1.0
},
(decodedText) => {
this.decodedText = decodedText
this.onHandle(decodedText)
},
(errorMessage) => {}
);
} catch (err) {
console.error('扫描启动失败:', err);
vant.Dialog.alert({
title: '扫描失败',
message: '无法启动扫码功能,请刷新页面重试'
}).then(() => {})
}
},
async closeScan() {
if (!this.html5QrCode || !this.scanStatus) return;
try {
await this.html5QrCode.stop();
this.scanStatus = false;
this.html5QrCode = null;
} catch (err) {
console.error('关闭失败:', err);
}
},
async onHandle(decodedText) {
// 防重:如果正在处理,直接忽略
if (this.isProcessing) return;
// 立即设置为处理中,并停止扫描
this.isProcessing = true;
await this.closeScan()
const { bizType, userId } = JSON.parse(decodedText)
this.result = await this.$axios.post('/platform/scan/handle', {
bizType: bizType,
params: JSON.stringify({ userId: userId, handle: false }),
})
this.resultVisible = true
},
resultConfirm() {
const { bizType, userId } = JSON.parse(this.decodedText)
this.$axios.post('/platform/scan/handle', {
bizType: bizType,
params: JSON.stringify({ userId: userId, handle: true }),
})
this.isProcessing = false;
this.initCamera()
}
},
async created() {
},
beforeDestroy() {
if (this.scanStatus) {
this.closeScan();
}
}
});
</script>
<!--#
}
#-->
@@ -79,6 +79,13 @@ const mine = {
</span>
</van-cell>
<van-cell is-link @click="qrCode" class="action-cell" v-if="codeValid()">
<span class="cell-title" slot="title">
<van-icon name="apps-o" class="cell-icon"></van-icon>
电子身份码
</span>
</van-cell>
<!-- <van-cell :value="$store.state.user.totalIntegral" is-link @click="$pjaxReplace('/platform/integral/records/h5')" class="action-cell">-->
<!-- <span class="cell-title" slot="title">-->
<!-- <van-icon name="points" class="cell-icon"></van-icon>-->
@@ -91,7 +98,12 @@ const mine = {
<div class="logout-wrapper">
<van-button round block color="#ee0a24" @click="logout" class="logout-btn">退出登录</van-button>
</div>
<van-action-sheet v-model="codeVisible" cancel-text="取消" title="电子身份码">
<qrcode :options="{ width: 300 }" :value="codeAddress" class="qrcode"></qrcode>
</van-action-sheet>
</div>
`,
computed: {
avatar() {
@@ -103,6 +115,13 @@ const mine = {
}
}
},
store,
data() {
return {
codeVisible: false,
codeAddress: '',
}
},
methods: {
logout() {
this.$dialog
@@ -113,10 +132,27 @@ const mine = {
.then(() => {
this.$store.dispatch("logout")
})
}
},
codeValid() {
if(this.$store.state.user.member === true || ['年薪制', '境外聘用', '劳务派遣一类', '劳务派遣二类'].includes(this.$store.state.user.preparedBy)) {
return true
}
return false
},
qrCode() {
this.codeVisible = true
this.codeAddress = JSON.stringify({userId: this.$store.state.user.id, bizType: 'culture'})
},
},
style() {
return /*language=CSS*/ `
.qrcode {
transition: opacity 0.5s ease;
display: block;
margin: 0 auto;
}
.home-mine {
background: #f7f8fa;
}