This commit is contained in:
Apcallover
2025-12-19 16:10:39 +08:00
10 changed files with 1658 additions and 1506 deletions

16
App.vue
View File

@@ -24,6 +24,7 @@ onLaunch((options) => {
getUserInfo(); getUserInfo();
useUserStore().changMiniProgramAppStatus(false); useUserStore().changMiniProgramAppStatus(false);
useUserStore().changMachineEnv(false); useUserStore().changMachineEnv(false);
useLocationStore().getLocationLoop()//循环获取定位
return; return;
} }
if (isY9MachineType()) { if (isY9MachineType()) {
@@ -32,7 +33,18 @@ onLaunch((options) => {
useUserStore().logOutApp(); useUserStore().logOutApp();
useUserStore().changMiniProgramAppStatus(true); useUserStore().changMiniProgramAppStatus(true);
useUserStore().changMachineEnv(true); useUserStore().changMachineEnv(true);
useLocationStore().getLocation(); (function loop() {
console.log('📍一体机尝试获取定位')
useLocationStore().getLocation().then(({longitude,latitude})=>{
console.log(`✅一体机获取定位成功:lng:${longitude},lat${latitude}`)
})
.catch(err=>{
console.log('❌一体机获取定位失败,30s后尝试重新获取')
setTimeout(() => {
loop()
}, 3000);
})
})()
uQRListen = new IncreaseRevie(); uQRListen = new IncreaseRevie();
inactivityManager = new GlobalInactivityManager(handleInactivity, 60 * 1000); inactivityManager = new GlobalInactivityManager(handleInactivity, 60 * 1000);
inactivityManager.start(); inactivityManager.start();
@@ -40,6 +52,7 @@ onLaunch((options) => {
} }
// 正式上线去除此方法 // 正式上线去除此方法
console.warn('浏览器环境'); console.warn('浏览器环境');
useLocationStore().getLocationLoop()//循环获取定位
useUserStore().changMiniProgramAppStatus(true); useUserStore().changMiniProgramAppStatus(true);
useUserStore().changMachineEnv(false); useUserStore().changMachineEnv(false);
useUserStore().initSeesionId(); //更新 useUserStore().initSeesionId(); //更新
@@ -57,6 +70,7 @@ onLaunch((options) => {
onMounted(() => {}); onMounted(() => {});
onShow(() => { onShow(() => {
console.log('App Show'); console.log('App Show');
}); });

View File

@@ -82,6 +82,7 @@ const { userInfo } = storeToRefs(useUserStore());
const { getUserResume } = useUserStore(); const { getUserResume } = useUserStore();
const { dictLabel, oneDictData } = useDictStore(); const { dictLabel, oneDictData } = useDictStore();
const openSelectPopup = inject('openSelectPopup'); const openSelectPopup = inject('openSelectPopup');
import { FileValidator } from '@/static/js/fileValidator.js'; //文件校验
const percent = ref('0%'); const percent = ref('0%');
const state = reactive({ const state = reactive({
@@ -278,15 +279,26 @@ function selectAvatar() {
sizeType: ['original', 'compressed'], sizeType: ['original', 'compressed'],
sourceType: ['album', 'camera'], sourceType: ['album', 'camera'],
count: 1, count: 1,
success: ({ tempFilePaths, tempFiles }) => { success: async (res) => {
$api.uploadFile(tempFilePaths[0], true) const tempFilePaths = res.tempFilePaths;
.then((res) => { const file = res.tempFiles[0];
res = JSON.parse(res);
if (res.msg) fromValue.avatar = res.msg; const imageValidator = new FileValidator();
})
.catch((err) => { try {
$api.msg('上传失败'); await imageValidator.validate(file);
});
$api.uploadFile(tempFilePaths[0], true)
.then((res) => {
res = JSON.parse(res);
if (res.msg) fromValue.avatar = res.msg;
})
.catch((err) => {
$api.msg('上传失败');
});
} catch (error) {
$api.msg(error);
}
}, },
fail: (error) => {}, fail: (error) => {},
}); });

View File

@@ -281,6 +281,8 @@ const { messages, isTyping, textInput, chatSessionID } = storeToRefs(useChatGrou
import successIcon from '@/static/icon/success.png'; import successIcon from '@/static/icon/success.png';
import useUserStore from '@/stores/useUserStore'; import useUserStore from '@/stores/useUserStore';
const { isMachineEnv } = storeToRefs(useUserStore()); const { isMachineEnv } = storeToRefs(useUserStore());
import {FileValidator} from "@/static/js/fileValidator.js" //文件校验
// hook // hook
// 语音识别 // 语音识别
const { const {
@@ -536,22 +538,29 @@ function uploadCamera(type = 'camera') {
count: 1, //默认9 count: 1, //默认9
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有 sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
sourceType: [type], //从相册选择 sourceType: [type], //从相册选择
success: function (res) { success: async (res)=> {
const tempFilePaths = res.tempFilePaths; const tempFilePaths = res.tempFilePaths;
const file = res.tempFiles[0]; const file = res.tempFiles[0];
// 继续上传
$api.uploadFile(tempFilePaths[0], true).then((resData) => { const imageValidator = new FileValidator()
resData = JSON.parse(resData); try {
console.log(file.type,'++') await imageValidator.validate(file)
if (isImage(file.type)) {
filesList.value.push({ $api.uploadFile(tempFilePaths[0], true).then((resData) => {
url: resData.msg, resData = JSON.parse(resData);
type: file.type, console.log(file.type,'++')
name: file.name, if (isImage(file.type)) {
}); filesList.value.push({
textInput.value = state.uploadFileTips; url: resData.msg,
} type: file.type,
}); name: file.name,
});
textInput.value = state.uploadFileTips;
}
});
} catch (error) {
$api.msg(error)
}
}, },
}); });
} }
@@ -560,25 +569,30 @@ function getUploadFile(type = 'camera') {
if (VerifyNumberFiles()) return; if (VerifyNumberFiles()) return;
uni.chooseFile({ uni.chooseFile({
count: 1, count: 1,
success: (res) => { success: async(res) => {
const tempFilePaths = res.tempFilePaths; const tempFilePaths = res.tempFilePaths;
const file = res.tempFiles[0]; const file = res.tempFiles[0];
const allowedTypes = config.allowedFileTypes || []; const allowedTypes = config.allowedFileTypes || [];
const size = $api.formatFileSize(file.size); const size = $api.formatFileSize(file.size);
if (!allowedTypes.includes(file.type)) {
return $api.msg('仅支持 txt md word pdf ppt csv excel 格式类型'); const imageValidator = new FileValidator({allowedExtensions:config.allowedFileTypes})
}
// 继续上传 try{
$api.uploadFile(tempFilePaths[0], true).then((resData) => { await imageValidator.validate(file)
resData = JSON.parse(resData);
filesList.value.push({ $api.uploadFile(tempFilePaths[0], true).then((resData) => {
url: resData.msg, resData = JSON.parse(resData);
type: file.type, filesList.value.push({
name: file.name, url: resData.msg,
size: size, type: file.type,
name: file.name,
size: size,
});
textInput.value = state.uploadFileTips;
}); });
textInput.value = state.uploadFileTips; }catch(error){
}); $api.msg(error)
}
}, },
}); });
} }

View File

@@ -470,7 +470,6 @@ const { columnCount, columnSpace } = useColumnCount(() => {
getJobRecommend('refresh'); getJobRecommend('refresh');
nextTick(() => { nextTick(() => {
waterfallsFlowRef.value?.refresh?.(); waterfallsFlowRef.value?.refresh?.();
useLocationStore().getLocation();
}); });
}); });

View File

@@ -129,7 +129,6 @@ const { columnCount, columnSpace } = useColumnCount(() => {
pageSize.value = 10 * (columnCount.value - 1); pageSize.value = 10 * (columnCount.value - 1);
nextTick(() => { nextTick(() => {
waterfallsFlowRef.value?.refresh?.(); waterfallsFlowRef.value?.refresh?.();
useLocationStore().getLocation();
}); });
}); });

View File

@@ -151,7 +151,6 @@ const { columnCount, columnSpace } = useColumnCount(() => {
pageSize.value = 10 * (columnCount.value - 1); pageSize.value = 10 * (columnCount.value - 1);
nextTick(() => { nextTick(() => {
waterfallsFlowRef.value?.refresh?.(); waterfallsFlowRef.value?.refresh?.();
useLocationStore().getLocation();
}); });
}); });

170
static/js/fileValidator.js Normal file
View File

@@ -0,0 +1,170 @@
const KNOWN_SIGNATURES = {
png: '89504E470D0A1A0A',
jpg: 'FFD8FF',
jpeg: 'FFD8FF',
gif: '47494638',
webp: '52494646',
docx: '504B0304',
xlsx: '504B0304',
pptx: '504B0304',
doc: 'D0CF11E0',
xls: 'D0CF11E0',
ppt: 'D0CF11E0',
pdf: '25504446',
txt: 'TYPE_TEXT',
csv: 'TYPE_TEXT',
md: 'TYPE_TEXT',
json: 'TYPE_TEXT',
};
export class FileValidator {
version = '1.0.0';
signs = Object.keys(KNOWN_SIGNATURES);
constructor(options = {}) {
this.maxSizeMB = options.maxSizeMB || 10;
if (options.allowedExtensions && Array.isArray(options.allowedExtensions)) {
this.allowedConfig = {};
options.allowedExtensions.forEach((ext) => {
const key = ext.toLowerCase();
if (KNOWN_SIGNATURES[key]) {
this.allowedConfig[key] = KNOWN_SIGNATURES[key];
} else {
console.warn(`[FileValidator] 未知的文件类型: .${key},已忽略`);
}
});
} else {
this.allowedConfig = {
...KNOWN_SIGNATURES,
};
}
}
_isValidUTF8(buffer) {
try {
const decoder = new TextDecoder('utf-8', {
fatal: true,
});
decoder.decode(buffer);
return true;
} catch (e) {
return false;
}
}
_bufferToHex(buffer) {
return Array.prototype.map
.call(new Uint8Array(buffer), (x) => ('00' + x.toString(16)).slice(-2))
.join('')
.toUpperCase();
}
_countCSVRows(buffer) {
const decoder = new TextDecoder('utf-8');
const text = decoder.decode(buffer);
let rowCount = 0;
let inQuote = false;
let len = text.length;
for (let i = 0; i < len; i++) {
const char = text[i];
if (char === '"') {
inQuote = !inQuote;
} else if (char === '\n' && !inQuote) {
rowCount++;
}
}
if (len > 0 && text[len - 1] !== '\n') {
rowCount++;
}
return rowCount;
}
_validateTextContent(buffer, extension) {
let contentStr = '';
try {
const decoder = new TextDecoder('utf-8', {
fatal: true,
});
contentStr = decoder.decode(buffer);
} catch (e) {
console.warn('UTF-8 解码失败', e);
return false;
}
if (contentStr.includes('\0')) {
return false;
}
if (extension === 'json') {
try {
JSON.parse(contentStr);
} catch (e) {
console.warn('无效的 JSON 格式');
return false;
}
}
return true;
}
validate(file) {
return new Promise((resolve, reject) => {
if (!file || !file.name) return reject('无效的文件对象');
if (file.size > this.maxSizeMB * 1024 * 1024) {
return reject(`文件大小超出限制 (最大 ${this.maxSizeMB}MB)`);
}
const fileName = file.name.toLowerCase();
const extension = fileName.substring(fileName.lastIndexOf('.') + 1);
const expectedMagic = this.allowedConfig[extension];
if (!expectedMagic) {
return reject(`不支持的文件格式: .${extension}`);
}
const reader = new FileReader();
reader.onload = (e) => {
const buffer = e.target.result;
let isSafe = false;
if (expectedMagic === 'TYPE_TEXT') {
if (this._validateTextContent(buffer, extension)) {
isSafe = true;
} else {
if (extension === 'json') {
return reject(`文件异常:不是有效的 JSON 文件`);
}
return reject(`文件异常:.${extension} 包含非法二进制内容或编码错误`);
}
if (extension === 'csv' && this.csvMaxRows > 0) {
const rows = this._countCSVRows(buffer);
if (rows > this.csvMaxRows) {
return reject(`CSV 行数超出限制 (当前 ${rows} 行,最大允许 ${this.csvMaxRows} 行)`);
}
}
} else {
const fileHeader = this._bufferToHex(buffer.slice(0, 8));
if (fileHeader.startsWith(expectedMagic)) {
isSafe = true;
} else {
return reject(`文件可能已被篡改 (真实类型与 .${extension} 不符)`);
}
}
if (isSafe) resolve(true);
};
reader.onerror = () => reject('文件读取失败,无法校验');
if (expectedMagic === 'TYPE_TEXT' && extension === 'json') {
reader.readAsArrayBuffer(file);
} else {
reader.readAsArrayBuffer(file.slice(0, 2048));
}
});
}
}
// 【demo】
// 如果传入了 allowedExtensions则只使用传入的否则使用全部 KNOWN_SIGNATURES
// const imageValidator = new FileValidator({
// maxSizeMB: 5,
// allowedExtensions: ['png', 'jpg', 'jpeg'],
// });
// imageValidator
// .validate(file)
// .then(() => {
// statusDiv.textContent = `检测通过: ${file.name}`;
// statusDiv.style.color = 'green';
// console.log('图片校验通过,开始上传...');
// // upload(file)...
// })
// .catch((err) => {
// statusDiv.textContent = `检测失败: ${err}`;
// statusDiv.style.color = 'red';
// });

File diff suppressed because it is too large Load Diff

View File

@@ -11,13 +11,15 @@ import config from '../config';
const defalutLongLat = { const defalutLongLat = {
longitude: 120.382665, longitude: 120.382665,
latitude: 36.066938 latitude: 36.066938,
} }
const useLocationStore = defineStore("location", () => { const useLocationStore = defineStore("location", () => {
// 定义状态 // 定义状态
const longitudeVal = ref(null) // 经度 const longitudeVal = ref(null) // 经度
const latitudeVal = ref(null) //纬度 const latitudeVal = ref(null) //纬度
const timer = ref(null)
const count = ref(0)
function getLocation() { // 获取经纬度两个平台 function getLocation() { // 获取经纬度两个平台
return new Promise((resole, reject) => { return new Promise((resole, reject) => {
@@ -66,6 +68,21 @@ const useLocationStore = defineStore("location", () => {
} }
}) })
} }
function getLocationLoop(gap = 1000 * 60 * 2) {
console.log(`🔄开始循环获取定位,间隔:${Math.floor(gap/1000)}`)
const run = () => {
count.value++
console.log(`📍第${count.value}次获取定位`)
getLocation()
}
run()
timer.value = setInterval(run,gap);
}
function clearGetLocationLoop(params) {
clearInterval(timer.value)
timer.value = null
}
function longitude() { function longitude() {
return longitudeVal.value return longitudeVal.value
@@ -78,9 +95,10 @@ const useLocationStore = defineStore("location", () => {
// 导入 // 导入
return { return {
getLocation, getLocation,
getLocationLoop,
clearGetLocationLoop,
longitudeVal, longitudeVal,
latitudeVal latitudeVal,
} }
}, { }, {
unistorage: true, unistorage: true,

View File

@@ -1,210 +0,0 @@
const KNOWN_SIGNATURES = {
png: "89504E470D0A1A0A",
jpg: "FFD8FF",
jpeg: "FFD8FF",
gif: "47494638",
webp: "52494646",
docx: "504B0304",
xlsx: "504B0304",
pptx: "504B0304",
doc: "D0CF11E0",
xls: "D0CF11E0",
ppt: "D0CF11E0",
pdf: "25504446",
txt: "TYPE_TEXT",
csv: "TYPE_TEXT",
md: "TYPE_TEXT",
json: "TYPE_TEXT"
};
export class FileValidator {
version = "1.0.0";
signs = Object.keys(KNOWN_SIGNATURES);
constructor(options = {}) {
this.maxSizeMB = options.maxSizeMB || 10;
if (options.allowedExtensions && Array.isArray(options.allowedExtensions)) {
this.allowedConfig = {};
options.allowedExtensions.forEach(ext => {
const key = ext.toLowerCase();
if (KNOWN_SIGNATURES[key]) {
this.allowedConfig[key] = KNOWN_SIGNATURES[key]
} else {
console.warn(`[FileValidator] 未知的文件类型: .${key},已忽略`)
}
})
} else {
this.allowedConfig = {
...KNOWN_SIGNATURES
}
}
}
_isValidUTF8(buffer) {
try {
const decoder = new TextDecoder("utf-8", {
fatal: true
});
decoder.decode(buffer);
return true
} catch (e) {
return false
}
}
_bufferToHex(buffer) {
return Array.prototype.map.call(new Uint8Array(buffer), x => ("00" + x.toString(16)).slice(-2)).join("")
.toUpperCase()
}
_countCSVRows(buffer) {
const decoder = new TextDecoder("utf-8");
const text = decoder.decode(buffer);
let rowCount = 0;
let inQuote = false;
let len = text.length;
for (let i = 0; i < len; i++) {
const char = text[i];
if (char === '"') {
inQuote = !inQuote
} else if (char === "\n" && !inQuote) {
rowCount++
}
}
if (len > 0 && text[len - 1] !== "\n") {
rowCount++
}
return rowCount
}
_validateTextContent(buffer, extension) {
let contentStr = "";
try {
const decoder = new TextDecoder("utf-8", {
fatal: true
});
contentStr = decoder.decode(buffer)
} catch (e) {
console.warn("UTF-8 解码失败", e);
return false
}
if (contentStr.includes("\0")) {
return false
}
if (extension === "json") {
try {
JSON.parse(contentStr)
} catch (e) {
console.warn("无效的 JSON 格式");
return false
}
}
return true
}
validate(file) {
return new Promise((resolve, reject) => {
if (!file || !file.name) return reject("无效的文件对象");
if (file.size > this.maxSizeMB * 1024 * 1024) {
return reject(`文件大小超出限制 (最大 ${this.maxSizeMB}MB)`)
}
const fileName = file.name.toLowerCase();
const extension = fileName.substring(fileName.lastIndexOf(".") + 1);
const expectedMagic = this.allowedConfig[extension];
if (!expectedMagic) {
return reject(`不支持的文件格式: .${extension}`)
}
const reader = new FileReader;
reader.onload = e => {
const buffer = e.target.result;
let isSafe = false;
if (expectedMagic === "TYPE_TEXT") {
if (this._validateTextContent(buffer, extension)) {
isSafe = true
} else {
if (extension === "json") {
return reject(`文件异常:不是有效的 JSON 文件`)
}
return reject(`文件异常:.${extension} 包含非法二进制内容或编码错误`)
}
if (extension === "csv" && this.csvMaxRows > 0) {
const rows = this._countCSVRows(buffer);
if (rows > this.csvMaxRows) {
return reject(`CSV 行数超出限制 (当前 ${rows} 行,最大允许 ${this.csvMaxRows} 行)`)
}
}
} else {
const fileHeader = this._bufferToHex(buffer.slice(0, 8));
if (fileHeader.startsWith(expectedMagic)) {
isSafe = true
} else {
return reject(`文件可能已被篡改 (真实类型与 .${extension} 不符)`)
}
}
if (isSafe) resolve(true)
};
reader.onerror = () => reject("文件读取失败,无法校验");
if (expectedMagic === "TYPE_TEXT" && extension === "json") {
reader.readAsArrayBuffer(file)
} else {
reader.readAsArrayBuffer(file.slice(0, 2048))
}
})
}
}
// FileValidator 使用文档
// FileValidator 是一个用于浏览器端的 JavaScript 文件校验类。它提供了比简单的后缀名检查更安全的文件验证机制。
// 主要特性
// 真实类型检测:通过读取文件二进制头部的“魔数”来验证文件类型,防止后缀名伪造。
// 文本内容安全:检测文本文件是否为有效的 UTF-8 编码,防止乱码或二进制文件伪装。
// JSON 语法校验:针对 JSON 文件,会自动尝试解析以确保格式正确。
// CSV 行数限制:支持限制 CSV 文件的最大行数(需手动配置)。
// 大小限制内置文件大小检查MB
// 按需配置:支持自定义允许的文件扩展名列表。
// 使用方法
// import { FileValidator } from './fileValidator.js';
// 1. 初始化校验器
// const validator = new FileValidator({
// maxSizeMB: 5, // 限制最大 5MB
// allowedExtensions: ['jpg', 'png', 'pdf', 'docx'] // 仅允许这些格式
// });
// // 2. 获取文件对象 (通常来自 input[type="file"])
// const fileInput = document.getElementById('file-upload');
// fileInput.addEventListener('change', async (event) => {
// const file = event.target.files[0];
// if (!file) return;
// try {
// // 3. 执行校验
// await validator.validate(file);
// console.log('✅ 文件校验通过,可以上传');
// // 在这里执行你的上传逻辑...
// } catch (errorMessage) {
// console.error('❌ 校验失败:', errorMessage);
// alert(errorMessage);
// }
// });
// 配置项:
// maxSizeMB: 允许的最大文件大小,单位 MB。
// allowedExtensions: 允许的文件后缀列表(不区分大小写)。如果不传,则允许所有内置支持的格式。
// // 允许所有支持的格式,限制 20MB
// const v1 = new FileValidator({ maxSizeMB: 20 });
// // 仅允许图片
// const v2 = new FileValidator({
// allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp']
// });
// 设置 CSV 最大允许 1000 行
// validator.csvMaxRows = 1000;
// 分类,扩展名,检测方式
// 图片,"png, jpg, jpeg, gif, webp",二进制头签名 (Magic Number)
// 文档,"pdf, docx, xlsx, pptx, doc, xls, ppt",二进制头签名 (Magic Number)
// 文本,"txt, csv, md",UTF-8 编码检测 + 无 Null 字节检测
// 数据,json,UTF-8 编码检测 + JSON.parse 语法校验