首页
留言
友链
关于
Search
1
思源笔记docker私有化部署及使用体验分享
2,529 阅读
2
windows11 远程提示:为安全考虑,已锁定该用户帐户,原因是登录尝试或密码更改尝试过多。
1,147 阅读
3
解决 nginxProxyManager 申请证书时的SSL失败问题
657 阅读
4
Pointer-Focus:一款功能强大的教学、录屏辅助软件
635 阅读
5
使用cspell对项目做拼写规范检查
598 阅读
Web前端
CSS
JavaScript
交互
Vue
小程序
后端
运维
项目
生活
其他
转载
软件
职场
登录
Search
标签搜索
docker
DevOps
magic-boot
Linux
酷壳
frp
RabbitMQ
gitlab
Node
git
工具
MybatisPlus
clickhouse
Syncthing
规范
前端
产品
nginx
markdown
axios
朱治龙
累计撰写
146
篇文章
累计收到
9
条评论
首页
栏目
Web前端
CSS
JavaScript
交互
Vue
小程序
后端
运维
项目
生活
其他
转载
软件
职场
页面
留言
友链
关于
搜索到
146
篇与
朱治龙
的结果
2024-05-12
RabbitMQ学习:①安装
背景近期参与公司的在线充值业务的功能开发,该业务涉及多个系统交互,采用MQ的方式实现跨系统通讯:而在我既往的项目经验中还未使用过 MQ,便利用工作之余对相关的知识点进行补充学习。本系列内容即是我的一个0基础入门学习记录,仅做参考。RabbitMQ 基本介绍RabbitMQ 是一个由 Erlang 语言开发的 AMQP 的开源实现。Erlang是为电话交换机编写的语言,天然对分布式和高并发支持良好。常用MQ对比比较项RabbitMQActiveMQRocketMQKafka公司/社区Broadcom Inc.Apache阿里Apache开发语言ErlangJavaJavaScala&Java协议支持AMQP,XMPP,SMTP,STOMPoPENwIRE,stomp,REST,XMPP,AMQP自定义自定义协议,社区封装了http协议支持客户端支持语言官方支持Erlang,Java,Ruby,PHP,.NET,GO,JS等,社区产出多种API,几乎支持所有语言Java,C,C++,Python,PHP,Perl,.NET等Java,C++官方支持Java,社区产出多种API,如PHP,Python等单机吞吐量万级(其次)万级(最差)十万级(最好)十万级(次之)消息延迟微秒级毫秒级毫秒级毫秒以内功能特性并发能力强,性能极其好,延时低,社区活跃,管理界面丰富老牌产品,成熟度高,文档较多MQ功能必要完备,扩展性佳只支持主要的MQ功能,主要为大数据领域场景安装 RabbitMQ为快速安装部署,使用 docker compose 方式运行,docker-compose.yaml文件内容如下:services: rabbitmq: image: rabbitmq:management restart: always container_name: rabbitmq ports: - 5672:5672 - 15672:15672 volumes: - ./data:/var/lib/rabbitmq使用 docker compose up -d 启动后,使用浏览器访问如下地址:http://localhost:15672/,显示如下界面则表示 RabbitMQ 运行成功:在上面的登录界面使用 guest 作为用户名和密码登录,打开如下图所示的主界面:
2024年05月12日
31 阅读
0 评论
0 点赞
2024-05-11
用 1 年时间赶超同龄人 38 倍的人生算法
成功的人都抓住了复利,我们可以从认证做好每一件小事开始,不断进步复利成长,用1年的时间超越同龄人38倍。
2024年05月11日
51 阅读
0 评论
0 点赞
2024-04-07
基于 Magic-api + Clickhouse 实现业务数据更新的项目记录
背景介绍项目有用到 Clickhouse 作为数仓,存储一些用户日常业务产生的大数据,下面先简单介绍一下我们这个任务的需求背景:我们的每个用户都会归属于某个用户组,并基于用户所在的计费组织实现产品使用过程中的消费等情况。而按照系统的设定用户初始注册时是没有归属用户组的,计费组织的主账号可以在控制台将用户绑定到该计费组下,也可以解绑,解绑后也可以绑定到其他用户组。为了更好的这个变更情况,我们在 Clickhouse 添加了一张名为 user_type 的表,每次该数据变更都会新增一条记录,该表的结构如下:CREATE TABLE user_type ( `user_id` Nullable(String), `present_type` Nullable(String), `pay_type` Nullable(String), `group_type` Nullable(String), `start_date` Nullable(Date), `end_date` Nullable(Date), `uni_key` Nullable(String) ) ENGINE = Log;实现方案本项目初期由使用 dbt + Clickhouse 的方式来实现,但是经实践运行一段时间后发现 dbt 做数据同步很方便,但是要添加一些业务逻辑就显得很棘手。为了解决 dbt 的问题,我们使用已搭建的 magic-api 来实现这个数据的更新,由于相关数据仅需一天一更新即可,所以我们可以直接利用 magic-api 自带的定时任务机制来实现更新。技术细节为便于相关业务逻辑在接口和定时任务中复用,我们将核心代码写在函数模块中:相关步骤核心代码如下步骤:1、从计费系统获取最新的userType信息var statSQL = `select e.*, CONCAT(e.user_id,'-',e.present_type,'-',e.pay_type,'-',e.group_type,'-',date_format(e.start_date,'%Y-%m-%d')) as uni_key FROM( SELECT a.user_id, CASE WHEN EXISTS ( SELECT 1 FROM ( SELECT t1.user_id user_id from b_contract t1 LEFT JOIN b_contract_item t2 ON t1.id = t2.contract_id WHERE t2.is_present = 0 and t2.received_payments > 0 GROUP BY t1.user_id UNION SELECT u2.user_id user_id from b_user as u1, b_user as u2 where u1.group_id=u2.group_id AND u1.user_id != u2.user_id AND EXISTS( SELECT 1 FROM (SELECT t1.user_id from b_contract t1 LEFT JOIN b_contract_item t2 ON t1.id = t2.contract_id WHERE t2.is_present = 0 and t2.received_payments > 0 GROUP BY t1.user_id) c WHERE c.user_id = u1.user_id ) ) d WHERE a.user_id = d.user_id ) then 'pay' else 'no pay' END as present_type, CASE WHEN EXISTS( SELECT 1 FROM( SELECT t1.user_id FROM b_user t1 , b_group t2 WHERE t1.user_id=t2.pay_user_id AND t2.pay_user_id IS NOT NULL )b WHERE a.user_id = b.user_id ) THEN 'master' ELSE 'slave' END as pay_type, CASE WHEN EXISTS(SELECT 1 FROM(SELECT t1.user_id FROM b_user t1 WHERE t1.group_id IS NOT NULL)b WHERE a.user_id = b.user_id) THEN 'group' ELSE 'no group' END as group_type, CURRENT_DATE as start_date, DATE(null) as end_date FROM b_user a )e` return db['NB'].select(statSQL)2、将上一步获取到的信息存储到Clickhouse 的一张临时表import log; import cn.hutool.core.date.DateUtil; import '@/statForProduction/userTypeStat/getLatestUserTypeData' as getLatestUserType; // ------------------- 一、创建临时表 ------------------- const TEMP_TABLE_NAME = 'user_type_temp' var checkExistRes = db['CH'].select(`SELECT 1 FROM system.tables WHERE database = 'dw' AND name = '${TEMP_TABLE_NAME}'`) log.info(checkExistRes.size() + '') // 不存在表的话就基于 user_type 表创建一张临时表 if (checkExistRes.size() === 0) { var initTemporaryTableSQL = `CREATE TABLE ${TEMP_TABLE_NAME} as user_type` db['CH'].update(initTemporaryTableSQL) } else { // 临时表存在则先清空临时表的数据,便于下一步将输入存入临时表 var truncateTemporaryTableSQL = `truncate table ${TEMP_TABLE_NAME}` db['CH'].update(truncateTemporaryTableSQL) } // ------------------- 二、获取最新的用户类型数据 ------------------- log.info(`============ 开始从计费系统获取最新的用户类型数据,该操作耗时较长,请耐心等待 ============`) var timer = DateUtil.timer() const userTypeList = getLatestUserType() log.info(`getLatestUserType cost time: ${timer.intervalPretty()}.`) // ------------------- 三、将数据存入临时表 ------------------- const BATCH_INSERT_COUNT = 1000 // 分批次入临时表,一次插入记录条数 var timer = DateUtil.timer() const allDataCount = userTypeList.size() if (allDataCount > 0) { log.info(`开始导入数据到临时表,待导入的总记录数为:${allDataCount},预计分${Math.ceil(allDataCount/BATCH_INSERT_COUNT)::int}批导入。`) const willInsertArr = [] var insertSQL = `insert into ${TEMP_TABLE_NAME}(user_id,present_type,pay_type,group_type,start_date,end_date,uni_key)` // 分批次插入临时表 for (index,userTypeItem in userTypeList) { willInsertArr.push(`('${userTypeItem.userId}','${userTypeItem.presentType}','${userTypeItem.payType}','${userTypeItem.groupType}','${userTypeItem.startDate}', null,'${userTypeItem.uniKey}')`) if (willInsertArr.size() === BATCH_INSERT_COUNT) { db['CH'].update(`${insertSQL} values${willInsertArr.join(',')}`) // 清空数据 willInsertArr.clear() log.info('Batch insert:' + index) } } // 不满整批次数据单独处理 if (willInsertArr.size() > 0) { db['CH'].update(`${insertSQL} values${willInsertArr.join(',')}`) // 清空数据 willInsertArr.clear() } } log.info(`insert latest user type to Temporary Table cost time: ${timer.intervalPretty()}.`) return true3、将临时表数据跟前一次最新的用户数据对比后,将有变更和新增的数据写入user_type表import log; import cn.hutool.core.date.DateUtil; const LATEST_TABLE_NAME = 'user_type_latest' // 用户最新类型数据表 const TEMP_TABLE_NAME = 'user_type_temp' // 该表存储从计费表获取到用户当前的用户类型数据,已在上一步获取数据完毕 // 一、从user_type表获取所有用户最新的用户类型数据并插入到用于计算的临时表 // 1.1 新建临时表,用于存储每个用户user_type 表中最新的用户类型数据 var checkExistRes = db['CH'].select(`SELECT 1 FROM system.tables WHERE database = 'dw' AND name = '${LATEST_TABLE_NAME}'`) log.info(checkExistRes.size() + '') // 不存在表的话就基于 user_type 表创建一张临时表 if (checkExistRes.size() === 0) { var initTemporaryTableSQL = `CREATE TABLE ${LATEST_TABLE_NAME} as user_type` db['CH'].update(initTemporaryTableSQL) } else { // 临时表存在则先清空临时表的数据,便于下一步将输入存入临时表 var truncateTemporaryTableSQL = `truncate table ${LATEST_TABLE_NAME}` db['CH'].update(truncateTemporaryTableSQL) } // 1.2 将最新数据写入临时表 // 该方式在数据量较大的情况下极有可能导致内存溢出,拟采取其他方案:在user_type 数据初始化的时候,将最新的用户类型数据存储到user_type_latest表,对比更新完成后将临时表的数据更新到user_type_latest便于下次对比 // const insertLatestDataSQL = `insert into ${LATEST_TABLE_NAME} SELECT user_type.user_id uid,user_type.present_type ,user_type.pay_type ,user_type.group_type,user_type.start_date,user_type.end_date,user_type.uni_key // FROM user_type, (SELECT user_type.user_id uid2,max(user_type.start_date) AS latestDate FROM user_type GROUP BY user_type.user_id) AS temp // WHERE user_type.start_date = temp.latestDate and uid = temp.uid2` // db['CH'].update(insertLatestDataSQL) // 二、两个临时表的数据做对比,并将最新数据更新到 user_type var timer = DateUtil.timer() // 2.1 更新有变更的数据 const changedInsertSQL = `insert into user_type select tuts.* from ${LATEST_TABLE_NAME} tutl left join ${TEMP_TABLE_NAME} tuts on tutl.user_id =tuts.user_id where tutl.present_type != tuts.present_type or tutl.pay_type != tuts.pay_type or tutl.group_type != tuts.group_type` timer.start("insertChangeData") db['CH'].update(changedInsertSQL) // 2.2 新增用户数据直接插入 timer.start("insertNewData") const insertNewUserSQL = `insert into user_type select * from ${TEMP_TABLE_NAME} tuts where tuts.user_id not in (select tutl.user_id from ${LATEST_TABLE_NAME} tutl) ` db['CH'].update(insertNewUserSQL) // 三、如果有数据更新,则将临时表的数据替换latest表 // 3.1 清理已有的数据 const truncateLatestTableSQL = `truncate table ${LATEST_TABLE_NAME}` db['CH'].update(truncateLatestTableSQL) // 3.2 从临时表导入最新的数据 const initialLatestTableDataSQL = `insert into ${LATEST_TABLE_NAME} select * from ${TEMP_TABLE_NAME}` db['CH'].update(initialLatestTableDataSQL) log.info(`insertChangeData cost time: ${timer.intervalPretty('insertChangeData')}`) log.info(`insertNewUser cost time: ${timer.intervalPretty('insertNewData')}`) // 四、清理临时表 const dropTempTableSQL = `drop table ${TEMP_TABLE_NAME}` db['CH'].update(dropTempTableSQL) return true 定义好相关函数后,我们可以直接在接口中用起来了,为此我定义了两个接口,一个接口用于数据初始化,一个接口用于手动更新数据:接口定义01数据初始化import log; import '@/statForProduction/userTypeStat/maintenance/clearUserTypeData' as clearUserTypeData import '@/statForProduction/userTypeStat/saveToTemporaryTable' as saveToTemporaryTable const LATEST_TABLE_NAME = 'user_type_latest' // 用户最新类型数据表 const TEMP_TABLE_NAME = 'user_type_temp' // 该表存储从计费表获取到用户当前的用户类型数据 // 一、清空所有user_type表的数据 clearUserTypeData() // 二、一次性写入所有 saveToTemporaryTable() // 三、将临时表的所有数据一次性写入user_type 表作为初始数据 const initialUserTypeDataSQL = `insert into user_type select * from ${TEMP_TABLE_NAME}` db['CH'].update(initialUserTypeDataSQL) // 四、将数据写入最新用户类型表,便于下一次做数据比对 // 4.1 基于 user_type 表 创建 user_type_latest 表 var checkExistRes = db['CH'].select(`SELECT 1 FROM system.tables WHERE database = 'dw' AND name = '${LATEST_TABLE_NAME}'`) log.info(checkExistRes.size() + '') // 不存在表的话就基于 user_type 表创建一张 if (checkExistRes.size() === 0) { var createLatestTableSQL = `CREATE TABLE ${LATEST_TABLE_NAME} as user_type` db['CH'].update(createLatestTableSQL) } else { // 表存在则先清空表的数据,便于下一步将最新的用户类型数据存入该表 var truncateLatestTableSQL = `truncate table ${LATEST_TABLE_NAME}` db['CH'].update(truncateLatestTableSQL) } // 4.2 插入该表的初始数据 const initialLatestTableDataSQL = `insert into ${LATEST_TABLE_NAME} select * from ${TEMP_TABLE_NAME}` db['CH'].update(initialLatestTableDataSQL) // 五、清理临时表 const dropTempTableSQL = `drop table ${TEMP_TABLE_NAME}` db['CH'].update(dropTempTableSQL) 02手工同步用户类型数据/** * 本接口用于手工临时同步数据用,日常使用定时任务自动同步操作即可 */ import '@/statForProduction/userTypeStat/saveToTemporaryTable' as saveToTemporaryTable import '@/statForProduction/userTypeStat/updateUserTypeData' as updateUserTypeData saveToTemporaryTable() updateUserTypeData()添加定时任务本任务用到的部分 Clickhouse SQL-- 判断数据表是否存在 SELECT 1 FROM system.tables WHERE database = 'dw' AND name = 'temp_user_type_session' -- 根据user_type 表创建一张名为 temp_user_type_session 的临时表 CREATE TABLE temp_user_type_session as user_type; -- 清空某数据表中的所有内容 truncate table temp_user_type_session; -- 查询所有用户最新的用户类型数据 SELECT user_type.user_id uid,user_type.present_type ,user_type.pay_type ,user_type.group_type,user_type.start_date,user_type.end_date,user_type.uni_key FROM user_type, (SELECT user_type.user_id uid2,max(user_type.start_date) AS latestDate FROM user_type GROUP BY user_type.user_id) AS temp WHERE user_type.start_date = temp.latestDate and uid = temp.uid2; -- 获取有差异的数据 select tutl.*,tuts.user_id user_id2, tuts.present_type present_type2, tuts.pay_type pay_type2, tuts.group_type group_type2, tuts.start_date start_date2,tuts.uni_key uni_key2 from temp_user_type_latest tutl left join temp_user_type_session tuts on tutl.user_id =tuts.user_id where tutl.present_type != tuts.present_type or tutl.pay_type != tuts.pay_type or tutl.group_type != tuts.group_type;
2024年04月07日
81 阅读
0 评论
0 点赞
2024-04-06
用户年度报告项目复盘
成果展示一图胜千言,我们先用视频来看实现好的最终效果,如果感兴趣我们再往下面了解技术实现细节。{gird column="2" gap="15"}{gird-item}{dplayer src="/usr/uploads/2024/07/1631300825.mp4"/}{mtitle title="并行版年度计算报告"/}预览网址(未对接接口版本,PC端直接访问会调整到扫码页面):http://para-yearly-report.work.zhuzhilong.com/{/gird-item}{gird-item}{dplayer src="/usr/uploads/2024/07/1449014954.mp4"/}{mtitle title="北龙版年度报告"/}预览网址(未对接接口版本,PC端直接访问会调整到扫码页面):http://blsc-yearly-report.work.zhuzhilong.com/{/gird-item}{/gird}缘起2023 年 12 月 20 日,网易云音乐 2023 年度听歌报告正式上线,一些是年度报告的部分截图:体验完网易云音乐的年度听歌报告后,感觉咱们也可以推出一款类似的报告,以下是跟运营同事的沟通截图:运营同事效率也是相当的高,第二天一早便梳理了我们年度报告的脑图:做好基础的准备工作后,当天跟领导确认了本项目的可行性:得到了领导的认可,就可以开展下一步的工作了。接着运营同事针对脑图的数据做了进一步的细化,梳理了需求文档:运营同事在需求文档中规划了每页展示的内容,下一步就可以交给设计的同事开展相关页面的设计工作了,为此专门拉了个小群沟通相关设计细节:不得不说负责设计的同事也是超级给力,很快就提供了高质量的设计稿:而且超级专业地提供了相关页面的动效设计说明:开干编写设计文档需求有了,设计也有了,我们就可以开工干活了。编码工作之前,我们需要根据需求和设计稿,进行技术相关的详细设计文档编写工作,这样才能更好的指导后续的编码开发工作,为此我详细编写了40页超过4000字的详细设计文档:规划年报数据在上面的详设文档中详细的规划了每一页所需的数据,每一页数据的来源,以及每个用户年报数据的组织形式、存储方式。有了这些数据做支撑后,就是怎么将这些数据按运营要求收集起来,由于数据分散在不同的业务系统里,如用户系统、计费系统、用户行为系统。需要将相关的数据收集后并归总,为此,我们设计了数据合并机制。用户年报页面实现现在需要的数据也有了,只需要将年报页面按照 UI 效果进行实施即可。由于整体项目采用前后端分离的机制,用户看到的年报页面也采用独立的 git 工程进行维护,完成代码后单独打包部署即可。整个前端页面,使用 vite 进行工程构建,使用 vue3 进行界面驱动,使用 swiper + vant-ui 实现基本页面布局,使用 echarts + vue-echarts 实现工程的图表效果,使用 animate.css 实现界面动效,工程的package.json 信息如下:{ "name": "console-yearlyreport-ui", "private": true, "version": "0.0.0", "type": "module", "scripts": { "dev": "vite", "dev:blsc": "vite --mode blsc.development", "devIstio": "vite build --mode devIstio", "devIstio:blsc": "vite build --mode blsc.devIstio", "stage": "vite build --mode stage", "stage:blsc": "vite build --mode blsc.stage", "build": "vite build", "build:report": "vite build", "build:blsc": "vite build --mode blsc.production" }, "dependencies": { "animate.css": "^4.1.1", "axios": "0.21.4", "dayjs": "^1.11.10", "echarts": "^5.4.3", "lodash": "^4.17.21", "pinia": "^2.1.7", "swiper": "^11.0.5", "vant": "^4.8.3", "vue": "^3.3.11", "vue-echarts": "^6.6.8" }, "devDependencies": { "@vitejs/plugin-vue": "^4.5.2", "rollup-plugin-visualizer": "^5.12.0", "sass": "^1.70.0", "terser": "^5.27.0", "vite": "^5.0.8", "vite-plugin-compression": "^0.5.1" } }前端工程的整体目录结构:下面就一些核心代码做一些梳理main.js该文件为项目入口文件,主要对环境做了些特殊处理,如未登录则跳转到登录页;PC端访问则跳转到扫码页面,引导用户使用手机扫码访问等。import { createApp } from 'vue' import { createPinia } from 'pinia' import 'animate.css' import './style.css' import '@/styles/index.scss' import { registerComponents } from './components/index' import initMatomo from './core/matomo/index.js' import App from './App.vue' const { VITE_APP_DIST_MODE: distMode } = import.meta.env /** * 检测浏览器环境,如果是PC端浏览器访问活动页则直接重定向到二维码扫码页面 */ const checkBrowser = () => { // 根据user-agent 判断是否跳转到pc页面 if (window.matchMedia && window.matchMedia("(min-width: 768px)").matches) { window.location.href = `pc_${distMode}.html` return false } return true } /** * 检测用户登录情况,如果localStorage 中不包含 token 则自动重定向到移动端授权页面 */ const checkLogin = () => { const token = localStorage.getItem('token') if (!token) { localStorage.setItem('authForExternalPage', `/yearly-report-2023/` + location.search) location.href='/auth' return false } return true } const checkEnv = async () => { if (!checkBrowser()) { return } // // 检测用户登录情况 if (!checkLogin()) { return } const pinia = createPinia() const app = createApp(App) // 注册全局组件 registerComponents(app) app.use(pinia) app.mount('#app') initMatomo(app, null) // 根节点添加样式名,便于对部分样式根据主题进行灵活控制 $('html').addClass(`theme-${distMode}`) } checkEnv() App.vue这是页面入口的核心文件,在这个文件里使用swiper组织了所有页面,并根据各种不同的条件做了不同展示处理(未登录、资源预加载时显示加载中、加载完成后展示封面页;封面页未勾选协议则无法滑动等); 并从整体上收集用户行为数据,完整代码如下:<template> <div v-if="existReportData === false"> <not-exist-data-page /> </div> <div v-else-if="pageResourceLoading" class="page-loading"> <van-circle v-model:current-rate="resourceLoadPercent" :rate="100" :speed="100" size="180px" :layer-color="themeColors.themeLight6" :color="gradientColor" :stroke-width="50" :text="resourceLoadPercent + '%'" /> <p class="loading-tips-text">页面加载中,请稍候</p> <!-- <van-progress :stroke-width="8" style="width: 90%" color="linear-gradient(to right, #be99ff, #7232dd)" :percentage="resourceLoadPercent" /> --> </div> <swiper v-else :slides-per-view="1" :direction="'vertical'" :space-between="0" effect="fade" @swiper="onSwiper" @slideChange="onSlideChange" @slideChangeTransitionEnd="onSlideChangeTransitionEnd" > <swiper-slide class="swiper-page0" data-page-name="封面页"><enter-page /></swiper-slide> <swiper-slide class="swiper-page1" data-page-name="第一页"><page1 /></swiper-slide> <swiper-slide class="swiper-page2" data-page-name="第二页"><page2 /></swiper-slide> <!-- <swiper-slide class="swiper-page3" data-page-name="第三页"><page3 /></swiper-slide> --> <swiper-slide class="swiper-page4" data-page-name="第四页"><page4 /></swiper-slide> <swiper-slide class="swiper-page5" data-page-name="第五页"><page5 /></swiper-slide> <swiper-slide class="swiper-page6" data-page-name="第六页"><page6 /></swiper-slide> </swiper> <img v-show="showUpArrow" src="/images/icon_arrow_up.png" class="arrow-up animate__animated animate__infinite animate__fadeInUp animate__slower" alt="提示上划" /> </template> <script setup> import { defineComponent, onMounted, ref, watch } from "vue"; import { Swiper, SwiperSlide } from "swiper/vue"; import "swiper/css"; import { checkReportData } from './api/yearlyReportAPI' import EnterPage from "./pages/EnterPage.vue"; import NotExistDataPage from "./pages/NotExistDataPage.vue"; import Page1 from "./pages/Page1.vue"; import Page2 from "./pages/Page2.vue"; // import Page3 from "./pages/Page3.vue"; import Page4 from "./pages/Page4.vue"; import Page5 from "./pages/Page5.vue"; import Page6 from "./pages/Page6.vue"; import { imagesLoad } from './core/imgLoad.min.js' import resourceImgs from './assets/imgResources.json' import themeColors from '@/core/themeColors.js' import { getShortUserId, getQueryString } from './core/utils.js' defineComponent({ components: { Swiper, SwiperSlide, }, }); const existReportData = ref() // 页面资源加载 const pageResourceLoading = ref(true) const resourceLoadPercent = ref(0) const gradientColor = { '0%': themeColors.themeLight3, '100%': themeColors.theme, } window.currentSwiper = null; const currentSwiperIndex = ref(0); const showUpArrow = ref(false); watch(currentSwiperIndex, () => { if (window.currentSwiper === null) { return; } if (currentSwiperIndex.value === 0) { window.currentSwiper.disable(); } // 封面页和最后一页不显示向上箭头 if (currentSwiperIndex.value === 0 || currentSwiperIndex.value === window.currentSwiper.slides.length - 1) { showUpArrow.value = false; return; } showUpArrow.value = true; }); const clearAnimation = () => { const prevSlideEl = window.currentSwiper.slides[window.currentSwiper.previousIndex]; $(prevSlideEl) .find(".js-animation") .each(function (idx, item) { const $this = $(item); const animation = $this.attr("data-animation"); $this .removeClass("animate__animated") .removeClass("animate__" + animation) .removeAttr("style"); }); }; const addAnimation = () => { clearAnimation(); const currentSlideEl = window.currentSwiper.slides[window.currentSwiper.activeIndex]; $(currentSlideEl) .find(".js-animation") .each(function (index, item) { var $this = $(item); var animation = $this.attr("data-animation"); var duration = $this.attr("data-duration"); var delay = $this.attr("data-delay"); $this .addClass("animate__animated") .addClass("animate__" + animation) .css({ "animation-duration": duration + "s", "animation-delay": delay + "s" }); }); // 入口页灯效单独处理 if (currentSwiperIndex.value === 0) { const $el = $('.swiper-page0 .bg-light') if ($el.hasClass('hide')) { setTimeout(() => { $el.removeClass('hide') }, 1500) } } }; /** * 添加Matomo 事件埋点 */ const addMatomoEvent = () => { const pageName = $('.swiper-slide-active').attr('data-page-name') if (pageName) { window._paq && window._paq.push(['trackEvent', '2023年度报告', '查看', pageName]) } } /** * 根据URL参数添加用户来源埋点 */ const addSourceEvent = () => { const sourceMap = { direct: '直接访问', cloud: '云桌面', console: 'PC端控制台', mconsole: '移动端控制台', kbs: 'KBS内容页', email: '邮件海报' } const sourceInQuery = getQueryString('utm_source') let fromLabel = '直接访问' if (sourceInQuery) { const sourceLabel = sourceMap[sourceInQuery] if (sourceLabel) { fromLabel = sourceLabel } } window._paq && window._paq.push(['trackEvent', '2023年度报告', '来源', fromLabel]) } const onSwiper = (s) => { addMatomoEvent() addSourceEvent() // 默认不允许用户滑动,仅同意协议并单击「点击回顾」按钮后才可滑动 s.disable(); if (window.currentSwiper === null) { window.currentSwiper = s; } addAnimation(); }; const onSlideChange = () => { currentSwiperIndex.value = window.currentSwiper.activeIndex; addAnimation(); if (currentSwiperIndex.value === 1) { $('.swiper-page0 .bg-light').addClass('hide') } }; const onSlideChangeTransitionEnd = () => { addMatomoEvent() } onMounted(async () => { // 检测用户数据 const checkRes = await checkReportData() if (checkRes.exist === false) { existReportData.value = false window._paq && window._paq.push(['trackEvent', '2023年度报告', '查看', '无数据页']) return } if (checkRes.lastUpdateTime) { localStorage.setItem('yearlyReportDataUpdateTime_' + getShortUserId(), checkRes.lastUpdateTime) } new imagesLoad(resourceImgs, function (num) { resourceLoadPercent.value = num }, function () { resourceLoadPercent.value = 100 setTimeout(() => { pageResourceLoading.value = false }, 300) }) }) </script> <style lang="scss" scoped> .page-loading { width: 100%; height: 100vh; padding-bottom: 20vh; box-sizing: border-box; display: flex; flex-direction: column; align-items: center; justify-content: center; background: $theme-light-8; // background-color: #F8FDFF; } .loading-tips-text { font-size: 16px; color:$theme-light-6; font-weight: bold; text-align: center; } .swiper { width: 100%; height: 100vh; } .swiper-slide { height: 100vh; } .arrow-up { position: absolute; width: 30px; margin-left: -15px; left: 50%; bottom: 30px; z-index: 100; pointer-events: none; } </style> 优先从本地缓存获取用户年报数据用户年度计算报告的数据属于生成后基本上不会怎么变化的数据,而这种活动一般也是短期的,为减少活动推广期间可能出现的高并发场景对服务器资源的损耗,拟将用户指标数据在第一次获取后缓存在客户端,客户端存在用户缓存数据的情况下将直接使用缓存数据进行界面展示,流程如下图所示:核心代码如下:/** * 截取部分userId用于作为用户特征串 */ export function getShortUserId() { const userId = localStorage.getItem('userid') if (userId) { if (userId.length > 40) { return userId.substring(0, 40) } return userId } } /** * 获取用户年报数据,优先从缓存里取 */ export async function getUserReportData() { const shortUserId = getShortUserId() const reportDataInLocal = localStorage.getItem('yearlyReportData_' + shortUserId) if (reportDataInLocal) { const reportDataObj = JSON.parse(reportDataInLocal) const lastUpdateTimeInLocal = reportDataObj.lastUpdateTime const lastUpdateTimeFromServer = localStorage.getItem('yearlyReportDataUpdateTime_' + shortUserId) if(lastUpdateTimeInLocal === lastUpdateTimeFromServer) { return reportDataObj.reportData } } const res = await getReportData() if (res.reportData) { res.reportData = JSON.parse(res.reportData) localStorage.setItem('yearlyReportData_' + shortUserId, JSON.stringify(res)) } return res.reportData }UI 效果以下图片为实际实现好的界面截图并行版本{gird column="4" gap="15"}{gird-item}{mtitle title="资源预加载"/}{/gird-item}{gird-item}{mtitle title="封面页"/}{/gird-item}{gird-item}{mtitle title="第一页"/}{/gird-item}{gird-item}{mtitle title="第二页"/}{/gird-item}{gird-item}{mtitle title="第二页查看机时"/}{/gird-item}{gird-item}{mtitle title="第三页"/}{/gird-item}{gird-item}{mtitle title="第四页(0点-6点)"/}{/gird-item}{gird-item}{mtitle title="第四页(6点-12点)"/}{/gird-item}{gird-item}{mtitle title="第四页(12点-18点)"/}{/gird-item}{gird-item}{mtitle title="第四页(18点-24点)"/}{/gird-item}{gird-item}{mtitle title="第五页"/}{/gird-item}{gird-item}{mtitle title="第六页"/}{/gird-item}{/gird}北龙版本{gird column="4" gap="15"}{gird-item}{mtitle title="资源预加载"/}{/gird-item}{gird-item}{mtitle title="封面页"/}{/gird-item}{gird-item}{mtitle title="第一页"/}{/gird-item}{gird-item}{mtitle title="第二页"/}{/gird-item}{gird-item}{mtitle title="第二页查看机时"/}{/gird-item}{gird-item} {mtitle title="第三页"/}{/gird-item}{gird-item}{mtitle title="第四页(0点-6点)"/}{/gird-item}{gird-item}{mtitle title="第四页(6点-12点)"/}{/gird-item}{gird-item}{mtitle title="第四页(12点-18点)"/}{/gird-item}{gird-item}{mtitle title="第四页(18点-24点)"/}{/gird-item}{gird-item} {mtitle title="第五页"/}{/gird-item}{gird-item} {mtitle title="第六页"/}{/gird-item}{/gird}运营后台为了便于运营人员核对用户指标数据及预览用户年报效果,我们使用 MagicBoot 快速开发了一个运营后台,相关界面效果如下:{gird column="2" gap="15"}{gird-item}{mtitle title="活动用户列表界面"/}{/gird-item}{gird-item}{mtitle title="查看用户指标数据,供运营核对指标数据"/}{/gird-item}{gird-item}{mtitle title="运营人员可扫码预览用户年度计算报告"/}{/gird-item}{gird-item}{mtitle title="后台年度报告预览效果"/}{/gird-item}{/gird}运营大屏为了便于运营人员及时了解用户访问大屏情况,我们使用 DataGear 制作了一个运营大屏,界面效果如下:详设文档本活动的技术开发工作能顺利开展,前期的详细设计是重中之重,详细设计中把方方面面的问题都考虑到了,在开发和上线阶段才能做到从容不迫,以下是详设文档附件(由于部分材料涉密,该文档仅部分人员可访问): 以下内容已加密,请输入密码查看:
2024年04月06日
48 阅读
0 评论
1 点赞
2024-02-28
酷壳(coolshell.cn)镜像站建设经验分享之五——镜像站制作
为了能保障镜像站能永久提供服务,不依赖于后端的数据库及中间件,我们需要将爬取到的正文内容做静态化发布,静态化发布后,我们可以将内容托管到 Gitee 或 Github。
2024年02月28日
52 阅读
0 评论
0 点赞
2024-02-28
酷壳(coolshell.cn)镜像站建设经验分享之四——解析文章正文html中的图片并离线下载图片到本地
上一步我们已经得到了文章详情所需的数据,要想保障酷壳站停掉后,我们仍然能完整的查看内容,我们需要将正文中的图片也都下载到本地,然后后续发布成镜像站的时候就不用外链图片资源了,这样也在最大程度保障原站无法访问的情况下,能通过镜像站完整的浏览博客。
2024年02月28日
51 阅读
0 评论
0 点赞
2024-02-28
酷壳(coolshell.cn)镜像站建设经验分享之三——分析爬取的文章html得到正文及相关的元数据
这个过程主要是分析文章 html,得到文章如下元数据:正文内容、作者、发布时间、分类列表、Tag词列表、浏览次数、评论次数,并将相关元数据存储到 crawler_article 表的对应字段里。
2024年02月28日
35 阅读
0 评论
0 点赞
2024-02-28
酷壳(coolshell.cn)镜像站建设经验分享之二——整站内容爬取
在前面的数据准备章节咱们已经在 magic-boot 基础工程上整合了 jsoup,而 jsoup 正是 java 领域数据爬取及 html 解析的神器,对于 html 的处理如同 JS 中的 jQuery 一般神一样的存在,而且一些 API 跟 jQuery 也比较接近,使用起来不得不说那叫一个丝滑。废话少说,咱们开始 show me the code。
2024年02月28日
25 阅读
0 评论
0 点赞
2024-02-25
酷壳(coolshell.cn)镜像站建设经验分享之一——准备工作
看酷壳网的一些评论,也有不少人像我一样,在担心以后域名或服务器到期后网站不能访问,为了解决这个后顾之忧,我便在工作之余开启了酷壳网站镜像之路,这一系列的文章也就是将这一过程做一个记录,也便于帮助有需要的朋友能够做到举一反三。
2024年02月25日
76 阅读
2 评论
0 点赞
2024-02-21
magic-boot 整合 Clickhouse 及在 magic-api 中的基本使用
项目有用到 Clickhouse 作为数仓, magic-boot 作为万金油般的存在,肯定是需要整合 Clickhouse 获取数据的,下面我们就开始吧。一、整合 clickhouse-jdbc 驱动根据clickhouse 官方文档的指引,在项目的 Maven 依赖管理文件(pom.xml)中的 dependencies 节点添加如下依赖项: <dependency> <groupId>com.clickhouse</groupId> <artifactId>clickhouse-jdbc</artifactId> <classifier>all</classifier> <version>0.6.0</version> </dependency>注:dependency 中一定要添加 <classifier>all</classifier>,否则会出现找不到依赖的className的异常二、magic-api 中添加数据源在 magic-api 主界面右侧的 DataSource 面板中,单击「+」按钮,打开「创建数据源」弹出层,如下图所示:相关表单项填写如下:名称:任意,只要自己能区别数据源即可Key:为便于在代码中引用,尽量采用简写URL:jdbc:(ch|clickhouse)[:<protocol>]://endpoint1,endpoint2,...?param1=value1¶m2=value2用户名:用户名密码:密码驱动类:com.clickhouse.jdbc.ClickHouseDriver类型:com.zaxxer.hikari.HikariDataSource。用Hikari 和 Druid 连接池测试都没碰到问题。本次测试填写后的连接池示例如下图所示:在 magic-api 中写测试代码进行功能验证创建数据表db['CH'].update(""" CREATE TABLE test_for_magic_boot ( `id` UUID, `user_name` String, `real_name` String, `birthday` Date, `gender` String ) ENGINE = MergeTree ORDER BY birthday SETTINGS index_granularity = 8192; """);添加测试数据// 添加数据要使用 update方法,使用insert 方法会报错。 // https://gitee.com/ssssssss-team/magic-api/issues/I4SQYW db['CH'].update(`insert into test_for_magic_boot(id,user_name,real_name,birthday,gender) values(#{uuid()},'shiyu', '时羽','1991-12-15', 'F'),(#{uuid()},'lint', '李宁涛','1985-11-19', 'M'),(#{uuid()},'gaowz', '高文中','1968-01-23', 'M')`)修改测试数据db['CH'].update(`update test_no_index set real_name='时大款' where user_name='shiyu'`) Clickhouse 更新操作有一些限制索引列不能进行更新分布式表不能进行更新不适合频繁更新或point更新查询数据return db['CH'].select('select * from test_for_magic_boot')删除数据db['CH'].update(`delete from test_for_magic_boot where user_name='lint'`)删除测试数据表db['CH'].update('drop table test_for_magic_boot');
2024年02月21日
182 阅读
0 评论
0 点赞
2024-01-04
[转载]你写文档吗?你为什么应该写文档?
本文主要是结合作者的项目实践来说明文档对于一个团队开发的重要性, 以及在提高效率节省时间方面的意义, 并且指出如何在实践开发中写文档与维护文档.
2024年01月04日
26 阅读
0 评论
0 点赞
2023-12-26
推荐一款统计代码行数的VSCode插件
基本介绍在撰写软著文档或做项目结项时我们一般都需要统计工程的代码行数,如果逐个文件进行统计的话,不仅费时费力,还不一定能得到准确的数据,所以我就在做这工作的时候找了下相关的工具,发现 VSCode 有一款好用的插件:Lines of Code(LOC)插件市场链接:https://marketplace.visualstudio.com/items?itemName=lyzerk.linecounterGithub 链接:https://github.com/alimozdemir/vscode-linecounter该插件当前的介绍信息见截图:基本使用安装后,可使用 Ctrl+Shift+P 打开 命令面板,然后输入 LineCount,显示如下选项:相关选项对应的是:LineCount:Count Workspace files:统计当前工作区所有文件的代码行数LineCount:Count current file:统计当前文件的代码行数选择 LineCount:Count Workspace files 后,系统将进行相关统计操作,统计完后会在当前工程的out目录生成 linecounter.txt 和 linecounter.json 文件,其中 linecounter.txt 文件内容如下:=============================================================================== EXTENSION NAME : linecounter EXTENSION VERSION : 0.2.7 ------------------------------------------------------------------------------- count time : 2023-12-27 09:21:44 count workspace : d:\GitRoot\SE\console\console-ui total files : 563 total code lines : 72734 total comment lines : 3925 total blank lines : 3554 statistics | extension| total code| total comment| total blank|percent| ------------------------------------------------------------------------- | .devIstio| 44| 2| 7| 0.060| | .stage| 34| 0| 0| 0.047| | .production| 41| 0| 0| 0.056| | | 193| 74| 42| 0.27| | .development| 36| 0| 0| 0.049| | .js| 13156| 1998| 752| 18| | .html| 2347| 6| 333| 3.2| | .md| 1660| 243| 419| 2.3| | .svg| 1743| 0| 107| 2.4| | .json| 3403| 7| 5| 4.7| | .yaml| 4153| 17| 626| 5.7| | .stg| 9| 2| 5| 0.012| | .blsc| 10| 2| 7| 0.014| | .conf| 103| 5| 27| 0.14| | .key| 30| 0| 0| 0.041| | .yml| 71| 0| 4| 0.098| | .crt| 28| 0| 0| 0.038| | .scss| 1778| 83| 312| 2.4| | .vue| 43126| 1467| 715| 59| | .css| 769| 19| 193| 1.1| ------------------------------------------------------------------------- .browserslistrc, code is 3, comment is 0, blank is 0. .editorconfig, code is 9, comment is 0, blank is 1. .env, code is 19, comment is 0, blank is 0. .env.blsc.development, code is 20, comment is 0, blank is 0. .env.blsc.devIstio, code is 20, comment is 0, blank is 0. .env.blsc.production, code is 23, comment is 0, blank is 0. .env.blsc.stage, code is 21, comment is 0, blank is 0. .env.development, code is 16, comment is 0, blank is 0. .env.devIstio, code is 14, comment is 0, blank is 0. .env.production, code is 18, comment is 0, blank is 0. .env.stage, code is 13, comment is 0, blank is 0. .eslintignore, code is 1, comment is 4, blank is 0. .eslintrc.js, code is 196, comment is 129, blank is 0. .gitignore, code is 20, comment is 0, blank is 2. .prettierrc, code is 6, comment is 0, blank is 0. admin.html, code is 21, comment is 2, blank is 0. cspell.json, code is 96, comment is 7, blank is 2. default.conf, code is 14, comment is 0, blank is 2. ... 统计的文件列表 src\views\userCenter\modals\WarningCcLimitModal.vue, code is 218, comment is 5, blank is 1. vite.config.js, code is 183, comment is 30, blank is 5. ===============================================================================从上面的信息不难看出当前工程内的文件总数及总代码行数, 包括注释行数及空行数。其中该插件有不少配置信息,可以在统计钱根据实际情况进行文件排除等场景,配置信息示例数据如下:{ "LineCount.showStatusBarItem": true, "LineCount.statistics": true, "LineCount.includes": [ "**/*" ], "LineCount.excludes": [ "**/.vscode/**", "**/node_modules/**" ], "LineCount.output": { "txt": true, "json": true, "csv": true, "md": true, "outdir":"out" }, "LineCount.sort": "filename", "LineCount.order": "asc", "LineCount.comment":[ { "ext": ["c","cpp","java"], "separator": { "linecomment": "//", "linetol":false, "blockstart": "/*", "blockend": "*/", "blocktol": false, "string":{ "doublequotes": true, "singlequotes": true } } }, { "ext": ["html"], "separator": { "blockstart": "<!--", "blockend": "-->", } } ] }
2023年12月26日
427 阅读
0 评论
0 点赞
2023-12-13
Jeepay开源版使用过程中踩过的坑
1、商户系统登录问题添加商户的时候有设置登录名,但是没有设置账号密码的位置,好不容易找到对商户重置密码的地方,但是那个勾选重置密码的复选框又超级容易被理解为用户下次登录需重置密码的配置项。勾选后有提示重置为默认密码,但是又没有说明默认密码是什么,然后非得要查看源码才知道是通过常量设置的默认密码为:jeepay6662、证书文件不存在问题好不容易登录商户系统了,进行支付测试的功能验证,提示证书文件不存在:整个应用部署过程完全是基于官方提供的 docker-compose.yml 文件,最后发现默认配置的 /home/jeepay/upload 目录根本就没有挂载到宿主机,修改 docker-compose.yml ,payment、manager、merchant 应用的 volumes 应用均挂载 /home/jeepay 目录,如: volumes: - ./jeepayData:/home/jeepay3、支付测试不显示二维码的问题支付测试时不显示支付二维码,发现HTTP请求中有个 404 请求:检查代码,确定应用存在对应的接口路径:查看docker 日志复现如下 error 信息:基于该信息可得知,nginx在接收到二维码图片请求时根本就没有请求到 jeepay-payment 这个后端服务,而是直接请求了root 目录中的文件,由此我们调整一下 代理的api接口的优先级,修改 jeepay-ui 根目录下的 default.conf.template 文件,在/api/ 前添加 ^~ ,nginx的路径匹配规则如下:/:通用匹配,任何请求都可以匹配=:用于不含正则表达式的uri前,要求请求字符串与uri严格匹配,如果匹配成功,就停止继续向下搜索并立即处理该请求。~:用于表示uri包含正则表达式,并且区分大小写。~*:用于表示uri包含正则表达式,并且不区分大小写。^~:用于不包含正则表达式的uri前,要求nginx服务器找到标识uri和请求字符串匹配度最高的location后,立即使用此location处理请求,而不再使用location块中的正则uri与请求字符串做匹配。!~和!~*:分别表示区分大小写不匹配和不区分大小写不匹配的正则优先级:= --> ^~ --> /* #当有多个包含/进行正则匹配时,选择正则表达式最长的location配置执行。多个location配置的情况下匹配顺序为: 首先匹配 =,其次匹配^~, 其次是按文件中顺序的正则匹配,最后是交给 /通用匹配。当有匹配成功时候,停止匹配,按当前匹配规则处理请求。注意:如果uri包含正则表达式,则必须要有 ~ 或者 ~ 标识。修改后的 default.conf.template 文件如下所示:server { listen 80; listen [::]:80; server_name localhost; root /workspace/; try_files $uri $uri/ /index.html; location ^~ /api/ { proxy_next_upstream http_502 http_504 error timeout invalid_header; proxy_pass http://$BACKEND_HOST; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } # favicon.ico location = /favicon.ico { log_not_found off; access_log off; } # robots.txt location = /robots.txt { log_not_found off; access_log off; } # assets, media location ~* \.(?:css(\.map)?|js(\.map)?|jpe?g|png|gif|ico|cur|heic|webp|tiff?|mp3|m4a|aac|ogg|midi?|wav|mp4|mov|webm|mpe?g|avi|ogv|flv|wmv)$ { expires 7d; access_log off; } # svg, fonts location ~* \.(?:svgz?|ttf|ttc|otf|eot|woff2?)$ { add_header Access-Control-Allow-Origin "*"; expires 7d; access_log off; } # gzip gzip on; gzip_vary on; gzip_proxied any; gzip_comp_level 6; gzip_types text/plain text/css text/xml application/json application/javascript application/rss+xml application/atom+xml image/svg+xml; } 4、公众号/小程序支付的URL多了一级/cashier应用部署完毕,进行支付测试时,采用「微信支付二维码」的方式已支付成功,但是采用「公众号/小程序」的支付方式,在扫码后,发现扫码后的页面显示空白,进一步排查,发现是由于页面的css和js资源文件404导致的,进一步排查,是由于请求的资源多了一级path,一下是问题排查过程:系统配置中的支付网关地址填写的是 https://jeepay-cashier.work.zhuzhilong.com:但是在使用支付测试功能,支付方式采用「公众号/小程序」进行支付测试时,生成的二维码如下:二维码识别后的地址为:https://jeepay-cashier.work.zhuzhilong.com/cashier/index.html#/hub/78d439f3140fe4047c7f8f6cda1048313636890021b83c0c167270dbce4fc2ff根据应用部署情况,比预期的访问路径多了 /cashier,查看源代码后,发现这个路径是写死在 com.jeequan.jeepay.core.model.DBApplicationConfig.java中的:去掉相关方法中的 /cashier 后,根据 docker-compose.yml 重新构建镜像及重启服务后,可正常支付。
2023年12月13日
129 阅读
0 评论
0 点赞
2023-10-31
magic-boot/magic-api 使用随记
magic-api 是一款非常优秀的快速开发框架,在做大屏的过程中找到的宝贝应用,可以用类JS 语法快速开发接口,能非常方便的操作数据库及处理一些复杂的业务逻辑,而 magic-boot 是基于 magic-api 开发的一款快速开发平台,提供了基本的用户鉴权、后台管理等功能。在实际项目过程中我基于 magic-boot 做了如下事项:1、通过Matomo的API定时同步数据至数仓做大数据分析2、采集coolshell.cn整站数据3、每周五定时推送企业微信消息,提醒同事写周报更多功能待进一步挖掘……下面是我在项目中有用到的技术点的一个记录,会在项目过程中不断更新,便于后续有其他项目用到的话,能快速查找运用。获取 系统设置/配置中心 模块设置的配置项import '@/configure/getBykey' as configure; var baseURL = configure('matomo.base-url'); var authToken = configure('matomo.auth-token');http请求数据示例import cn.hutool.json.JSONUtil import org.springframework.util.StringUtils import http; import log; import '@/configure/getBykey' as configure; // 从配置中心获取接口所需数据 var baseURL = configure('matomo.base-url'); var authToken = configure('matomo.auth-token'); // 组转请求URL var reqURL = `${baseURL}?module=API&method=${method}&format=JSON&token_auth=${authToken}`; if (!StringUtils.isEmpty(params)) { reqURL += `&${params}`; } log.info(`reqURL:${reqURL}`); // 请求数据 var resData = http.connect(reqURL).contentType('application/json').get().getBody(); return resData;在原基础上增加http请求出错重试机制import cn.hutool.json.JSONUtil import org.springframework.util.StringUtils import cn.hutool.core.date.DateUtil import cn.hutool.core.thread.ThreadUtil import http import log import '@/configure/getBykey' as configure; // 从配置中心获取接口所需数据 var baseURL = configure('matomo.base-url'); var authToken = configure('matomo.auth-token'); // 组转请求URL var reqURL = `${baseURL}?module=API&method=${method}&format=JSON&token_auth=${authToken}`; if (!StringUtils.isEmpty(params)) { reqURL += `&${params}`; } // log.info(`reqURL:${reqURL}`); var requestStartTime = DateUtil.now(); var successFlag = '' var exceptionContent = '' // 请求数据 var resData = '' // 最大重试次数 var MAX_RETRY_COUNT = 5 // 当前重试次数 var retryCount = 0; while(retryCount < MAX_RETRY_COUNT && successFlag !== 'Y') { try { resData = http.connect(reqURL).contentType('application/json').get().getBody(); successFlag = 'Y' } catch(e) { successFlag = 'N' ThreadUtil.sleep(1000); retryCount++ exceptionContent = e.getMessage(); } } var requestEndTime = DateUtil.now(); db.table('matomo_sync_log').insert({ 'apiMethod' : method, 'requestParams' : params, 'responseContent': resData.asString(), 'exceptionContent': exceptionContent, 'requestTime':requestStartTime, 'responseTime': requestEndTime, 'retryCount':retryCount, 'successFlag': successFlag}); return resData;分页获取数据在获取一些详情数据的时候,存在数据量超大的情况,一次性获取所有数据极有可能会导致数据库及应用挂掉,即便不挂掉的情况下,也会超长事件才会响应结果,所以采用分页获取还是很有必要的。下面的代码是在实际项目中分页调用Matomo的接口获取输入然后将接口返回的数据,结构化处理后,保存到本地数据库。 import cn.hutool.json.JSONUtil import log; import '@/dmcfns/sendMatomoRequest' as getMatomoData; var PAGE_SIZE=10 // 每页获取记录数,获取后批量入库 var currentPage = 0// 当前页 var needLoad = true // 继续加载数据标识,当当前页加载的内容小于PAGE_SIZE时则不再加载 while(needLoad) { var resData = getMatomoData('Live.getLastVisitsDetails', `period=day&date=${date}&idSite=${siteId}&doNotFetchActions=1&filter_offset=${PAGE_SIZE * currentPage}&filter_limit=${PAGE_SIZE}`) if (resData.asString().startsWith("[")) { var siteDatas = JSONUtil.parseArray(resData); var siteData = []; for (index, site in siteDatas) { var siteObj = siteDatas.getJSONObject(index); var visitId = siteObj.getStr("idVisit"); var visitorId = siteObj.getStr("visitorId"); var visitIp = siteObj.getStr("visitIp"); var longitude = siteObj.getStr("longitude"); var latitude = siteObj.getStr("latitude"); var userId = siteObj.getStr("userId"); var country = siteObj.getStr("country"); var referrerName = siteObj.getStr("referrerName"); var visitProps = JSONUtil.toJsonStr(siteObj); db.table('matomo_daily_visit').insert({ date, siteId, visitId, visitorId, visitIp, longitude, latitude, userId, country, referrerName, visitProps }) } if (siteDatas.size() === PAGE_SIZE) { currentPage++ } else { needLoad = false } } }使用多数据源操作数据库db['ZR'].table('crawler_list').insert({ pageURL: linkURL, articleTitle:linkTitle })根据主键更新部分字段内容var updateMap = { id: visitItem.id, ipCountry: country, ipProvince: province, ipCity: city } db.table('matomo_daily_visit').primary('id').update(updateMap)修改某个字段的值db.table("sys_user").column("isLogin", isLogin).where().eq("id",id).update()推送消息至企业微信机器人import http import log // 测试机器人 // var ROBOT_URL = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxx' // 超算云研发部2023 微信群的eHour 机器人 var ROBOT_URL = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=xxxx' var msg = { "msgtype": "text", "text": { "content": """为便于公司开展项目成本核算相关工作,请各位同事及时登录eHour系统录入本周工作工时,如有系统使用相关问题可联系 XXX,感谢配合[抱拳][抱拳]\neHour系统链接如下:http://172.18.3.xxx/""", "mentioned_list":["@all"] } } http.connect(ROBOT_URL).body(msg).post(); log.info('eHour消息推送成功') // 以下cron表达式为每周五16:30分执行 00 30 16 * * 05读取Excel文件并转换为jsonimport cn.hutool.poi.excel.ExcelUtil import request import log var datas = ExcelUtil.getReader(new ByteArrayInputStream(request.getFile('file').getBytes())).readAll() var sourceDatas = datas::stringify::json导出Excelimport cn.hutool.poi.excel.ExcelWriter; import cn.hutool.poi.excel.ExcelUtil; import cn.hutool.json.JSONUtil; import log; import response import java.io.*; var list = db['MDC'].select("select * from crawler_resource limit 10") //通过hutool工具创建的excel的writer,默认为xls格式 ExcelWriter writer= ExcelUtil.getWriter(); var bos = new ByteArrayOutputStream(); log.info("List:\n" + JSONUtil.toJsonPrettyStr(list)) try { //一次性写出内容,使用默认样式,强制输出标题 writer.write(list,true); writer.flush(bos,true); } finally { bos.flush(); writer.close(); } return response.download(bos.toByteArray(), "crawler_resource_list.xlsx");数据库事务处理db.transaction(() => { if (archived_at == null) { //项目取消归档 //推送取消归档的通知 db.table("project_tasks").primary("project_id").primary("archived_follow").update({ project_id: project_id, archived_follow: 1, archived_at: null, archived_follow: 0, }) } else { //项目归档 db.table("project_tasks").primary("project_id").primary("archived_at").update({ project_id: project_id, archived_at: null, archived_at: archived_at, archived_follow: 1, }) } db.table("projects").primary("id").update({ id: project_id, archived_at: archived_at, archived_userid: userid }) })
2023年10月31日
131 阅读
0 评论
0 点赞
2023-10-30
docker compose 部署Umami
很长一段时间是用的cnzz做的网站访问统计,功能强大,分析结果对于小白用户也超级友好,自从被阿里收购后,整合成Umeng的一部分勉强还能用,但是自从开启收费(收割用户)模式后,高昂的价格,无疑把我们这种小白个人用户完全隔离在外了。然后用了一段时间的百度统计,感觉也是不尽如人意,然后就只好另辟蹊径,调研了市面主流的流量统计工具(也就调研了Matomo 和Umami)后,选择了Umami 作为个人流量统计工具,主要是Matomo不少明细数据是存储的Binary数据,不便于通过SQL直观的查看,相对于Matomo而言,Umami 算是轻量级别的,UI 界面也更现代化。Umami 支持 PostgreSQL 和 MySQL 两种数据库,分别对应不同的Docker 镜像。ProstgreSQL:docker pull ghcr.io/umami-software/umami:postgresql-latestMySQL:docker pull ghcr.io/umami-software/umami:mysql-latest由于我的服务器上面已安装 MySQL 客户端,就直接采用 MySQL 的镜像。ghcr.io 是 GitHub 的 Docker 镜像仓库,国内环境可能在pull 时会碰到些网络方面的问题,我是通过一台境外的服务器pull 后,然后 push 到本人的 Docker 私服进行下载的,也可以采用导出备份后在导入的方式。如果你在pull过程中也存在这方面网络的问题的话,也推荐使用这个方式。docker-compose.ymlversion: "3.8" services: umami: image: ghcr.io/umami-software/umami:mysql-latest # image: hub.work.zhuzhilong.com/apps/umami:mysql container_name: umami restart: unless-stopped volumes: - ../hosts:/etc/hosts environment: - DATABASE_URL=mysql://DB_USERNAME:DB_PASSWORD@DB_HOST:DB_PORT/umami - DATABASE_TYPE=mysql - APP_SECRET=umami2023 - TZ=Asia/Shanghai networks: - net-zzl ports: - 8202:3000 networks: net-zzl: name: bridge_zzl external: true 使用 docker compose up -d 启动后,可使用默认的管理员账号登录:用户名:admin密码:umami登录后即可修改密码及添加站点了。以下是整合后的部分界面截图:顺便说下,umami的表结构比较简单,访问用户的IP信息都没有存表,如果有复杂运营场景的话,还是推荐使用 Matomo 之类的功能更强大的工具。当前版本(2.8.0)只有11张表:{mtitle title="2023-12-18更新"/}应用升级近日登录umami时提示最新发布了2.9.0 版本,而根据更新日志中的内容有提到可以查看访客的城市信息了,便及时更新了下,使用docker compose 的方式更新超级简单,主要执行如下命令:docker compose pull docker compose up --force-recreate提示数据库更新成功:然后重启应用即可
2023年10月30日
107 阅读
0 评论
0 点赞
2023-10-30
docker compose 部署 中微子代理(NeutrinoProxy)
近期在开源中国有看到 Neutrino-proxy 的一些介绍,了解到 NeutrinoProxy 是一款基于 Netty 的内网穿透工具,官方的介绍信息如下:基本介绍中微子代理 (neutrino-proxy) 是一款基于 netty 的内网穿透神器。该项目采用最为宽松的 MIT 协议,因此您可以对它进行复制、修改、传播并用于任何个人或商业行为。Gitee 地址:https://gitee.com/dromara/neutrino-proxy官网地址:http://neutrino-proxy.dromara.org服务端管理后台截图:主要特点:1、流量监控:首页图表、报表管理多维度流量监控。全方位掌握实时、历史代理数据。2、用户 / License:支持多用户、多客户端使用。后台禁用实时生效。3、端口池:对外端口统一管理,支持用户、License 独占端口。4、端口映射:新增、编辑、删除、禁用实时生效。5、Docker:服务端支持 Docker 一键部署。6、SSL 证书:支持 SSL,保护您的信息安全。7、域名映射:支持绑定子域名,方便本地调试三方回调8、采用最为宽松的 MIT 协议,免去你的后顾之忧之前一直有在用 FRP 作为内网穿透工具,用了很多年也确实非常好用,不过 FRP 在可视化管理方面比较欠缺,虽然提供了dashboard,但是只提供了代理端口查看及浏览统计方面的功能,不能提供多用户方面的管控。而 Neutrino 正好弥补了这方面的不足。以下是使用 docker compose 部署相关的代码,仅作为记录。服务端:docker-compose.ymlversion: '3.8' services: app: image: registry.cn-hangzhou.aliyuncs.com/asgc/neutrino-proxy:latest container_name: neutrino-proxy restart: always networks: - net-zzl ports: - 9000-9200:9000-9200/tcp - 9201:8888 volumes: - ./config:/root/neutrino-proxy/config networks: net-zzl: name: bridge_zzl external: true ./config/app.ymlneutrino: data: db: type: mysql # 自己的数据库实例,创建一个空的名为'neutrino-proxy'的数据库即可,首次启动服务端会自动初始化 url: jdbc:mysql://DB_HOST:3306/neutrino-proxy?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&useAffectedRows=true&useSSL=false driver-class: com.mysql.jdbc.Driver # 数据库帐号 username: DB_USERNAME # 数据库密码 password: DB_PASSWORD客户端官网文档不推荐使用docker的方式部署,但是考虑到要部署Java 环境之类的,对宿主机而言是挺麻烦的,还是试着通过docker 的方式部署的客户端,经过验证也是 OK 的。docker-composeversion: '3.8' services: app: image: aoshiguchen/neutrino-proxy-client:latest container_name: neutrino-proxy-client restart: always network_mode: host volumes: - ./config:/root/neutrino-proxy/config./config/app.ymlneutrino: proxy: logger: # 日志级别 level: ${LOG_LEVEL:info} tunnel: # 线程池相关配置,用于技术调优,可忽略 thread-count: 50 # 隧道SSL证书配置 key-store-password: ${STORE_PASS:123456} jks-path: ${JKS_PATH:classpath:/test.jks} # 服务端IP,这里替换成主机IP或域名 server-ip: ${SERVER_IP:proxy.xxx.com} # 服务端端口(对应服务端app.yml中的tunnel.port、tunnel.ssl-port) server-port: ${SERVER_PORT:9002} # 是否启用SSL(注意:该配置必须和server-port对应上) ssl-enable: ${SSL_ENABLE:true} # 客户端连接唯一凭证,这里替换成key license-key: ${LICENSE_KEY:ec7e9906cXXXXXX6430895c37fec75cd4e11} # 客户端唯一身份标识(可忽略,若不设置首次启动会自动生成) client-id: ${CLIENT_ID:workServer} # 是否开启隧道传输报文日志(日志级别为debug时开启才有效) transfer-log-enable: ${CLIENT_LOG:false} # 重连设置 reconnection: # 重连间隔(秒) interval-seconds: 10 # 是否开启无限重连(未开启时,客户端license不合法会自动停止应用,开启了则不会,请谨慎开启) unlimited: false client: udp: # 线程池相关配置,用于技术调优,可忽略 boss-thread-count: 5 work-thread-count: 20 # udp傀儡端口范围 puppet-port-range: 10000-10500 # 是否开启隧道传输报文日志(日志级别为debug时开启才有效) transfer-log-enable: ${CLIENT_LOG:false}实现后的效果截图:
2023年10月30日
116 阅读
0 评论
0 点赞
2023-10-19
Mybatis-Plus 分页功能实现流程
Mybatis 自带分页功能,但是该分页功能是基于内存的分页,也就是会讲所有符合条件的数据查询出来,然后在从内存中获取当前页的信息,这种方式在数据量大的情况下会存在严重的性能问题。我们通过 Mybatis-Plus 自带的分页插件可以很好的解决这个问题,实现步骤记录如下:1、 添加配置类,示例内容如下:package com.paratera.protect.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * Mybatis Plus 配置类,主要用于继承分页插件 * @author 朱治龙 * @date 2023-10-19 23:14:00 */ @Configuration public class MyBatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } } 2、使用分页方法查询2.1、selectPage2.1.1 示例代码 @Test void testSelectPage() { QueryWrapper<Staff> qw = Wrappers.query(); qw.ge("age", 30); Page<Staff> page = new Page<Staff>(1, 2); // 不查记录数 // page.setSearchCount(false); Page<Staff> pageData = staffMapper.selectPage(page, qw); System.out.println("总页数:" + pageData.getPages()); System.out.println("总记录数:" + pageData.getTotal()); pageData.getRecords().forEach(System.out::println); }2.1.2 查询结果DEBUG==> Preparing: SELECT COUNT(*) AS total FROM staff WHERE (age >= ?) DEBUG==> Parameters: 30(Integer) TRACE<== Columns: total TRACE<== Row: 4 DEBUG<== Total: 1 DEBUG==> Preparing: SELECT id,name,age,email,mobile,manager_id,create_time FROM staff WHERE (age >= ?) LIMIT ? DEBUG==> Parameters: 30(Integer), 2(Long) TRACE<== Columns: id, name, age, email, mobile, manager_id, create_time TRACE<== Row: 1087982257332887553, 大boss, 40, 001@paratera.com, 13888886666, null, 2019-01-11 14:20:20 TRACE<== Row: 1094590409767661570, 张雨琪, 31, zjq@blsc.com, 18684700070, 1088248166370832385, 2019-01-14 09:15:15 DEBUG<== Total: 2 总页数:2 总记录数:4 Staff(id=1087982257332887553, name=大boss, age=40, email=001@paratera.com, mobile=13888886666, managerId=null, createTime=2019-01-11T14:20:20) Staff(id=1094590409767661570, name=张雨琪, age=31, email=zjq@blsc.com, mobile=18684700070, managerId=1088248166370832385, createTime=2019-01-14T09:15:15) 2.2、selectMapsPage2.2.1 示例代码 @Test void testSelectMapsPage() { QueryWrapper<Staff> qw = Wrappers.query(); qw.ge("age", 30); Page<Map<String, Object>> page2 = new Page<>(1, 2); Page<Map<String, Object>> pageData2 = staffMapper.selectMapsPage(page2, qw); System.out.println("总页数:" + pageData2.getPages()); System.out.println("总记录数:" + pageData2.getTotal()); pageData2.getRecords().forEach(System.out::println); }2.2.2 查询结果DEBUG==> Preparing: SELECT COUNT(*) AS total FROM staff WHERE (age >= ?) DEBUG==> Parameters: 30(Integer) TRACE<== Columns: total TRACE<== Row: 4 DEBUG<== Total: 1 DEBUG==> Preparing: SELECT id,name,age,email,mobile,manager_id,create_time FROM staff WHERE (age >= ?) LIMIT ? DEBUG==> Parameters: 30(Integer), 2(Long) TRACE<== Columns: id, name, age, email, mobile, manager_id, create_time TRACE<== Row: 1087982257332887553, 大boss, 40, 001@paratera.com, 13888886666, null, 2019-01-11 14:20:20 TRACE<== Row: 1094590409767661570, 张雨琪, 31, zjq@blsc.com, 18684700070, 1088248166370832385, 2019-01-14 09:15:15 DEBUG<== Total: 2 总页数:2 总记录数:4 {create_time=2019-01-11T14:20:20, name=大boss, mobile=13888886666, id=1087982257332887553, age=40, email=001@paratera.com} {create_time=2019-01-14T09:15:15, manager_id=1088248166370832385, name=张雨琪, mobile=18684700070, id=1094590409767661570, age=31, email=zjq@blsc.com} 3、附加说明使用分页插件查询时默认是会执行两条 SQL,一条获取当前页的数据,一条获取总记录数。在某些场景下(如瀑布流模式),只需要获取当前页的内容即可,不需要总记录数相关分页数值,此时可在创建 Page 时,第三个参数给值为 false:Page(long current, long size, boolean searchCount)。也可使用 page.setSearchCount(false);
2023年10月19日
105 阅读
0 评论
0 点赞
2023-10-18
Mybatis-Plus 自定义SQL
有的时候使用条件构造器自定义SQL满足不了我们的需求,我们既想使用 Wrapper,又想使用SQL,MP 对这种方式也提供了支持,MP 版本号应≥3.0.7。下面是该方案的实现记录:实现方案一:Mapper接口中使用@Select注解1、Mapper 示例代码如下:import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.toolkit.Constants; import com.paratera.protect.entity.Staff; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.List; /** * @author 朱治龙 * @date 2023-10-17 11:46:00 */ public interface StaffMapper extends BaseMapper<Staff> { @Select("select * from staff ${ew.customSqlSegment}") List<Staff> selectAll(@Param(Constants.WRAPPER)Wrapper<Staff> wrapper); }2、调用示例代码: @Test void testSelfSQL() { LambdaQueryWrapper<Staff> lqw = Wrappers.lambdaQuery(Staff.class); lqw.eq(Staff::getName, "朱治龙").and(wq2 -> wq2.lt(Staff::getAge, 40).or().isNotNull(Staff::getEmail)); List<Staff> staffList = staffMapper.selectAll(lqw); staffList.forEach(System.out::println); }3、输出结果DEBUG==> Preparing: select * from staff WHERE (name = ? AND (age < ? OR email IS NOT NULL)) DEBUG==> Parameters: 朱治龙(String), 40(Integer) TRACE<== Columns: id, name, age, email, mobile, manager_id, create_time TRACE<== Row: 1714166763984199681, 朱治龙, 36, zhuzl@blsc.cn, 15084978453, 1088248166370832385, 2023-10-17 14:29:38 DEBUG<== Total: 1 Staff(id=1714166763984199681, name=朱治龙, age=36, email=zhuzl@blsc.cn, mobile=15084978453, managerId=1088248166370832385, createTime=2023-10-17T14:29:38)实现方案二:使用xml1、配置xml文件的存放路径。再application.yml中添加 xml 文件的引用路径配置mybatis-plus: mapper-locations: - classpath:/mapper/*Mapper.xml2、在resources目录下添加 mapper 目录,并新建 mapper 文件,如文件名为 StaffMapper.xml,示例内容为:<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.paratera.protect.dao.StaffMapper"> <select id="selectByXml" resultType="com.paratera.protect.entity.Staff"> select * from staff ${ew.customSqlSegment} </select> </mapper>3、Mapper 接口中添加方法,示例代码如下:package com.paratera.protect.dao; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.toolkit.Constants; import com.paratera.protect.entity.Staff; import org.apache.ibatis.annotations.Param; import java.util.List; /** * @author 朱治龙 * @date 2023-10-17 11:46:00 */ public interface StaffMapper extends BaseMapper<Staff> { List<Staff> selectByXml(@Param(Constants.WRAPPER)Wrapper<Staff> wrapper); }4、调用示例代码: @Test void testSelfSQL2() { LambdaQueryWrapper<Staff> lqw = Wrappers.lambdaQuery(Staff.class); lqw.eq(Staff::getName, "朱治龙").and(wq2 -> wq2.lt(Staff::getAge, 50).or().isNotNull(Staff::getEmail)); List<Staff> staffList = staffMapper.selectByXml(lqw); staffList.forEach(System.out::println); }5、输出结果DEBUG==> Preparing: select * from staff WHERE (name = ? AND (age < ? OR email IS NOT NULL)) DEBUG==> Parameters: 朱治龙(String), 50(Integer) TRACE<== Columns: id, name, age, email, mobile, manager_id, create_time TRACE<== Row: 1714166763984199681, 朱治龙, 36, zhuzl@blsc.cn, 15084978453, 1088248166370832385, 2023-10-17 14:29:38 DEBUG<== Total: 1 Staff(id=1714166763984199681, name=朱治龙, age=36, email=zhuzl@blsc.cn, mobile=15084978453, managerId=1088248166370832385, createTime=2023-10-17T14:29:38)
2023年10月18日
49 阅读
0 评论
0 点赞
2023-09-03
超全的正则表达式速查手册
一、校验数字的表达式数字:^[0-9]*$n位的数字:^\d{n}$至少n位的数字:^\d{n,}$m-n位的数字:^\d{m,n}$零和非零开头的数字:^(0|[1-9][0-9]*)$非零开头的最多带两位小数的数字:^([1-9][0-9]*)+(.[0-9]{1,2})?$带1-2位小数的正数或负数:^(-)?\d+(.\d{1,2})?$正数、负数、和小数:^(-|+)?\d+(.\d+)?$有两位小数的正实数:^[0-9]+(.[0-9]{2})?$有1~3位小数的正实数:^[0-9]+(.[0-9]{1,3})?$非零的正整数:^[1-9]\d$ 或 ^([1-9][0-9]){1,3}$或 ^\+?[1-9][0-9]*$非零的负整数:^-[1-9][]0-9″$ 或 ^-[1-9]\d$ 非负整数:^\d+$ 或 ^[1-9]\d*|0$非正整数:^-[1-9]\d*|0$或 ^((-\d+)|(0+))$非负浮点数:^\d+(.\d+)?$或 ^[1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0$非正浮点数:^((-\d+(.\d+)?)|(0+(.0+)?))$或 ^(-([1-9]\d*\.\d*|0\.\d*[1-9]\d*))|0?\.0+|0$正浮点数:^[1-9]\d.\d|0.\d[1-9]\d$或 ^(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*))$负浮点数:^-([1-9]\d.\d|0.\d[1-9]\d)$或 ^(-(([0-9]+\.[0-9]*[1-9][0-9]*)|([0-9]*[1-9][0-9]*\.[0-9]+)|([0-9]*[1-9][0-9]*)))$浮点数:^(-?\d+)(.\d+)?$或 ^-?([1-9]\d*\.\d*|0\.\d*[1-9]\d*|0?\.0+|0)$二、校验字符的表达式汉字:^[\u4e00-\u9fa5]{0,}$英文和数字:^[A-Za-z0-9]+$或 ^[A-Za-z0-9]{4,40}$长度为3-20的所有字符:^.{3,20}$由26个英文字母组成的字符串:^[A-Za-z]+$由26个大写英文字母组成的字符串:^[A-Z]+$由26个小写英文字母组成的字符串:^[a-z]+$由数字和26个英文字母组成的字符串:^[A-Za-z0-9]+$由数字、26个英文字母或者下划线组成的字符串:^\w+$ 或 ^\w{3,20}中文、英文、数字包括下划线:^[\u4E00-\u9FA5A-Za-z0-9_]+$中文、英文、数字但不包括下划线等符号:^[\u4E00-\u9FA5A-Za-z0-9]+$或 ^[\u4E00-\u9FA5A-Za-z0-9]{2,20}$可以输入含有^%&’,;=?$” 等字符:[^%&’,;=?$\x22]+禁止输入含有~的字符: [^~\x22]+其它.匹配除 n 以外的任何字符。/[\u4E00-\u9FA5]/ 汉字/[\uFF00-\uFFFF]/ 全角符号/[\u0000-\u00FF]/ 半角符号三、特殊需求表达式Email 地址:1^\w+([-+.]\w+)@\w+([-.]\w+).\w+([-.]\w+)$1域名:[a-zA-Z0-9][-a-zA-Z0-9]{0,62}(/.[a-zA-Z0-9][-a-zA-Z0-9]{0,62})+/.?InternetURL:[a-zA-z]+://[^\s] 或 ^http://([\w-]+\.)+[\w-]+(/[\w-./?%&=])?$手机号码:^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$电话号码(“XXX-XXXXXXX”、”XXXX-XXXXXXXX”、”XXX-XXXXXXX”、”XXX-XXXXXXXX”、”XXXXXXX”和”XXXXXXXX):^((\d{3,4}-)|\d{3.4}-)?\d{7,8}$国内电话号码(0511-4405222、021-87888822):\d{3}-\d{8}|\d{4}-\d{7}身份证号(15位、18位数字):^\d{15}|\d{18}$短身份证号码(数字、字母x结尾):^([0-9]){7,18}(x|X)?$或 ^\d{8,18}|[0-9x]{8,18}|[0-9X]{8,18}?$帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$密码(以字母开头,长度在6~18之间,只能包含字母、数字和下划线):^[a-zA-Z]\w{5,17}$强密码(必须包含大小写字母和数字的组合,不能使用特殊字符,长度在8-10之间):^(?=.\d)(?=.[a-z])(?=.*[A-Z]).{8,10}$日期格式:^\d{4}-\d{1,2}-\d{1,2}一年的12个月(01~09和1~12):^(0?[1-9]|1[0-2])$一个月的31天(01~09和1~31):^((0?[1-9])|((1|2)[0-9])|30|31)$钱的输入格式有四种钱的表示形式我们可以接受:”10000.00” 和 “10,000.00”, 和没有 “分” 的 “10000” 和 “10,000”:^[1-9][0-9]*$这表示任意一个不以0开头的数字,但是,这也意味着一个字符”0”不通过,所以我们采用下面的形式:^(0|[1-9][0-9]*)$一个0或者一个不以0开头的数字.我们还可以允许开头有一个负号:^(0|-?[1-9][0-9]*)$这表示一个0或者一个可能为负的开头不为0的数字.让用户以0开头好了.把负号的也去掉,因为钱总不能是负的吧.下面我们要加的是说明可能的小数部分:^[0-9]+(.[0-9]+)?$必须说明的是,小数点后面至少应该有1位数,所以”10.”是不通过的,但是 “10” 和 “10.2” 是通过的:^[0-9]+(.[0-9]{2})?$这样我们规定小数点后面必须有两位,如果你认为太苛刻了,可以这样:^[0-9]+(.[0-9]{1,2})?$这样就允许用户只写一位小数.下面我们该考虑数字中的 逗号 了,我们可以这样:^[0-9]{1,3}(,[0-9]{3})*(.[0-9]{1,2})?$1到3个数字,后面跟着任意个 逗号+3个数字,逗号成为可选,而不是必须:^([0-9]+|[0-9]{1,3}(,[0-9]{3})*)(.[0-9]{1,2})?$备注:这就是最终结果了,别忘了+可以用*替代如果你觉得空字符串也可以接受的话(奇怪,为什么?)最后,别忘了在用函数时去掉去掉那个反xml文件:^([a-zA-Z]+-?)+[a-zA-Z0-9]+.[x|X][m|M][l|L]$中文字符的正则表达式:[\u4e00-\u9fa5]双字节字符:^\x00-\xff)空白行的正则表达式:\n\s*\r (可以用来删除空白行)HTML标记的正则表达式:<(\S?)[^>]>.?</\1>|<.? /> (网上流传的版本太糟糕,上面这个也仅仅能部分,对于复杂的嵌套标记依旧无能为力)首尾空白字符的正则表达式:^\s|\s (可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式)腾讯QQ号:[1-9][0-9]{4,}(腾讯QQ号从10000开始)中国邮政编码:[1-9]\d{5}(?!\d)(中国邮政编码为6位数字)IP地址:\d+.\d+.\d+.\d+(提取IP地址时有用)IP地址:((?:(?:25[0-5]|2[0-4]\d|[01]?\d?\d).){3}(?:25[0-5]|2[0-4]\d|[01]?\d?\d))IP-v4地址:\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?).){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b (提取IP地址时有用)校验IP-v6地址: (([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))子网掩码:((?:(?:25[0-5]|2[0-4]\d|[01]?\d?\d)\.){3}(?:25[0-5]|2[0-4]\d|[01]?\d?\d))校验日期:^(?:(?!0000)[0-9]{4}-(?:(?:0[1-9]|1[0-2])-(?:0[1-9]|1[0-9]|2[0-8])|(?:0[13-9]|1[0-2])-(?:29|30)|(?:0[13578]|1[02])-31)|(?:[0-9]{2}(?:0[48]|[2468][048]|[13579][26])|(?:0[48]|[2468][048]|[13579][26])00)-02-29)$(“yyyy-mm-dd“ 格式的日期校验,已考虑平闰年。)抽取注释:<!–(.*?)–>查找CSS属性:^\s[a-zA-Z\-]+\s[:]{1}\s[a-zA-Z0-9\s.#]+[;]{1}提取页面超链接:(<a\s(?!.\brel=)[^>])(href=”https?:\/\/)((?!(?:(?:www\.)?’.implode(‘|(?:www\.)?’, $follow_list).’))[^” rel=”external nofollow” ]+)”((?!.\brel=)[^>])(?:[^>])>提取网页图片:\< [img][^\\>][src] = [\”\’]{0,1}([^\”\’\ >]*)提取网页颜色代码:^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$文件扩展名效验:^([a-zA-Z]\:|\\)\\([^\\]+\\)[^\/:?”<>|]+\.txt(l)?$判断IE版本:^.MSIE 5-8?(?!.Trident\/[5-9]\.0).*$
2023年09月03日
20 阅读
0 评论
0 点赞
2023-08-27
修改gitlab账号密码
由于本人的gitlab为个人所用,隔久了未用,之前设置的密码不能登录了,便试着在数据库层面重置密码,本文主要对重置gitlab管理员密码的过程进行记录
2023年08月27日
20 阅读
0 评论
0 点赞
1
2
3
...
8