commit
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user