Files
xiaotutongxue/frontend-pc
lovife 15b819746b feat(mp): M3-01 门禁核验(名单是闸门)
M3 的第一块:学生必须先过名单核验,才能开始填报任何东西。

交付:
· POST /api/v1/mp/roster/match(§5.4)—— 姓名+证件号与名册匹配,命中才放行并绑定
· common/util/IdCardUtil —— 身份证校验位(三态裁决)
· support/ClientIp —— 取真实调用方 IP(限流用,X-Forwarded-For 第一段)
· module/roster/entity/CollectRoster —— 名册实体(M2 会继续用)

四条守住的规则:
1) 失败提示必须统一(40007),不区分"姓名错"还是"号码错"。
   分开提示等于交付了一个身份证号枚举器:用任意姓名+目标号码试,
   若提示"姓名不对"就说明号码在名单里,一个个试就能把全校号码试出来。
   这不是体验取舍,是安全底线。E2E 专门断言两种失败的 msg 字符串完全一致。
2) 绑定并发安全:UPDATE ... WHERE bind_user_id IS NULL + 看影响行数。
   不要改成"先 SELECT 判空再 UPDATE"——中间有窗口,两人会同时通过检查,
   后绑的人静默覆盖先绑的人,而先绑的学生之后再也进不去。
   E2E 用两个线程同时冲同一行,断言"恰好一个成功、一个 40072"(实测 [0, 40072])。
3) 证件号明文只在 match() 里出现一次,失败日志只记 HMAC 与 uid(日志被翻出来
   同样是泄露);响应里永远只有 idCardMask,E2E 断言整个响应 JSON 不含明文。
4) 已绑定自己的人重复核验是幂等的 —— 学生刷新页面不该看到"该信息已被登记"。

IdCardUtil 的价值在三态而不在对错:
   真实数据集实测:名册 28 人校验位全通过(可信基准);某两个学生的文件名证件号
   校验位失败(确定是号码错,可自动改正);另一学生的两个号码都通过(是两个真人,
   串档实锤,必须人工介入)。后两种看起来都是"号码对不上",处理策略却完全相反——
   一个能放心自动修,另一个自动修就是把两个真人的材料合并。区分它们的唯一办法
   就是校验位。所以对外暴露 Verdict{VALID,CHECKSUM_FAILED,MALFORMED} 而非 boolean。

   1900 年下界是业务判断,不是国标要求:网上流传的"示例有效身份证号"里有一批
   出生年份在 1800 年代(如常被引用的 440524188001010014 → 1880 年),它们校验位
   合法但不可能是认定考生。只算校验位会把它们判成 VALID。这个坑写 E2E 时真踩到了。

E2E 怎么拿 token:门禁要求已登录的学生,而登录(/mp/login)需要真实的 wx.login
code,脚本拿不到。所以用 dev 配置里的 JWT 密钥自己签一个 MP token(HS512,
claims 与 JwtService.issue 同构)—— 与 e2e-m0 用 dev RSA 公钥加密登录密码是同一类
做法:用开发配置里的凭据手搓测试夹具,不是绕过认证逻辑。prod 密钥不在仓库里,
这个手法自然失效。顺带:读配置要读两个文件,issuer 在主 application.yml 里,
只读 dev 会漏掉它,而症状是 401,完全指不到"配置没读到"。

回归抓到我一条脆断言:
   e2e-m1-qrcode.py 的"token TTL 是 7000 秒"在连续第二次运行时失败(TTL=4633,
   正好是两次运行的时间差)——它假设 token 是刚取的,实际沿用了缓存。
   已改成"先清缓存强制重取,再断言 TTL∈[6900,7000]",连跑两次都稳定通过。
   教训与前面几次同类:断言要确定性地反映被测行为,而不是碰巧在某种顺序下通过。

已知缺口(写进交接摘要):
· /mp/login 未做:它需要真实 wx.login code,脚本测不了,只能在小程序里测,
  下一轮连同小程序核验页一起做。
· 形近字候选(FR-504c)未实现,needConfirmName 恒为 false。
· 名册导入未做(M2),E2E 用 SQL 直接造名册行。
· 小程序端还没有核验页(落地页的「开始填报」目前是明确的"开发中"提示)。

验收:e2e-m3-gate.py 29/29(含并发绑定与限流);
九条 E2E 合计 418 项断言全通过。
2026-09-29 10:46:21 +08:00
..

Gi Admin Pro

star
[![license](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)

简介

Gi Admin Pro 是一个基于 Vue3、Vite、TypeScript、Arco Design Vue、Pinia、VueUse 等的免费中后台模版,它使用了最新的前端技术栈,内置丰富的主题配置,有着极高的代码规范,基于 mock 实现的动态数据展示,开箱即用的模板,也可用于学习参考。

Gi 前缀含义: G:代表全局 i:代表我的

Gi 用来定义全局组件前缀,如 GiTable、GiForm、GiPageLayout等

特性

  • 最新技术栈:使用 Vue3 / Vite 等前端前沿技术开发,使用高效率的 npm 包管理器
  • TypeScript:应用程序级 JavaScript 的语言
  • 主题:丰富可配置的主题、暗黑模式
  • 代码规范:丰富的规范插件及极高的代码规范

预览

平台 预览地址
github Gi Admin Pro 预览地址
账号 密码
管理员 admin 123456
用户 user 123456

代码仓库

平台 预览地址 仓库地址
gitee(码云) -- Gitee 仓库地址
github Gi Admin Pro 预览地址 Github 仓库地址

项目示例图

安装使用

  • 安装依赖
npm install
  • 运行
npm run dev
  • 打包
npm run build

开源作者

Lin

常见问题

为什么安装依赖不成功?

检查node版本,最好使用原生镜像npm

还原镜像

npm config set registry https://registry.npmjs.org/

为什么选择 Arco 组件库,而不是 Element Plus?

Element Plus 对比 Arco design

为什么全局组件使用前缀 Gi?

全局组件设置了按需引入,使用前缀,方便和局部组件做区分

为什么组件使用单词大写开头 (PascalCase)命名写法?

本项目.vue文件名以及在模板使用中,均采用大写开头 (PascalCase)命名方式

参考 Vue2 官网-风格指南: https://v2.cn.vuejs.org/v2/style-guide/

组件命名:单文件组件的文件名应该要么始终是单词大写开头 (PascalCase),要么始终是横线连接 (kebab-case)

其他优点:方便搜索(横线连接 (kebab-case)对搜索没那么方便)

为什么 css 类名推荐横线连接 (kebab-case)

参考大部分大网站,都是这个命名规则,不使用: .myClass这种

页面显示异常?

页面必须要保留一个根元素!!!

Vue3 权限管理对路由进行排序和格式化处理方式

使用 xe-utils 这个 js 库,简化数据处理

文章地址

页面无法缓存?

请检查页面是否配置了name,且名称是否与数据一致

defineOptions({ name: 'AboutIndex' })
{
  path: '/about/index',
  name: 'AboutIndex', // 检查name是否一致
  component: () => import('@/views/about/index.vue')
}

项目规范

.vue 文件行数规范

一般来说,一个 .vue 文件行数建议不超过 400 行,超过建议组件化拆分

vue 相关的命名

<script setup lang="ts">
const isEdit = ref(false)

// 不推荐
const title = computed(() => {
  return isEdit.value ? '编辑' : '新增'
})

// 推荐 能一行就尽量写一行
const title = computed(() => (isEdit.value ? '编辑' : '新增'))
</script>
<script setup lang="ts">
// 表单建议使用 form 命名(简洁),不必要使用 formData, 同时使用 reactive
const form = reactive({
  name: '',
  phone: ''
})
</script>
<script setup lang="ts">
// 如果属性比较多
const getInitForm = () => ({
  name: '',
  phone: '',
  email: '',
  sex: 1,
  age: ''
})

const form = reactive(getInitForm())

// 重置form
const resetForm = () => {
  for (const key in form) {
    delete form[key]
  }
  Object.assign(form, getInitForm())
}
</script>
<script setup lang="ts">
import { useAppStore, useUserStore } from '@/stores'
import { useLoading } from '@/hooks'

// stores 或 hooks 的使用命名规则定义
const appStore = useAppStore()
const userStore = useUserStore()

const { loading, setLoading } = useLoading()
</script>

写法技巧

尽量使用三元表达式

// 优化前
let isEdit = true
let title = ''
if (isEdit) {
  title = '编辑'
} else {
  title = '新增'
}

// 优化后
let title = isEdit ? '编辑' : '新增'

善用 includes 方法

// 优化前
if (type === 1 || type === 2 || type === 3) {
}

// 优化后,此种方式在vue模板也可使用
if ([1, 2, 3].includes(type)) {
}

使用箭头函数简化函数

// 优化前
function add(num1, num2) {
  return num1 + num2
}

// 优化后
const add = (num1, num2) => num1 + num2

尽量减少 if else if

<script setup lang="ts">
// 比例进度条颜色 尽量减少 if else if
const getProportionColor = (proportion: number) => {
  if (proportion < 30) return 'danger'
  if (proportion < 60) return 'warning'
  return 'success'
}
</script>
// 优化前
const status = 200
const message = ''
if (status === 200) {
  message = '请求成功'
} else if (status === 404) {
  message = '请求出错'
} else if (status === 500) {
  message = '服务器错误'
}

// 优化后
const status = 200
const messageMap = {
  200: '请求成功',
  404: '请求出错',
  500: '服务器错误'
}
const message = messageMap[status]

如果函数参数超过两个,建议优化

<script setup lang="ts">
function createUser(name, phone, age) {
  console.log('姓名', name)
  console.log('手机', phone)
  console.log('年龄', age)
}

// 这种方式在使用的时候可读性很差,扩展性差,而且不易于维护
createUser('张三', '178****2828', 20)

function createUser2({ name, phone, age }) {
  console.log('姓名', name)
  console.log('手机', phone)
  console.log('年龄', age)
}

// 以对象传参更直观,更好扩展和维护
createUser2({ name: '张三', phone: '178****2828', age: 20 })
</script>

接口调用书写

写法一

不需要 loading,不需要错误打印的 情况

<script setup lang="ts">
import { ref } from 'vue'
import { getUserList as getUserListApi, type UserItem } from '@/apis' // 同名可以使用别名

const userList = ref<UserItem[]>([])
const getUserList = async () => {
  const res = await getUserListApi()
  console.log('如果异步成功,则会打印这行文字,否则不会打印这行文字,也不会往下执行')
  userList.value = res.data
}
</script>

// getUserListApi 是一个 Promise 异步函数,Promise 最后只有 成功 / 失败 两种状态 // getUserListApi 是基于 axios
封装的,在 axios 响应拦截器做了处理 // 当 res.success === false 的时候 Promise.reject(),
也就异步失败,异步失败不会往下执行

写法二

需要 loading,需要错误打印的情况

<script setup lang="ts">
import { ref } from 'vue'
import { getUserList as getUserListApi, type UserItem } from '@/apis' // 同名可以使用别名

const loading = ref(false)
const userList = ref<UserItem[]>([])
const getUserList = async () => {
  try {
    loading.value = true
    const res = await getUserListApi()
    console.log('如果异步成功,则会打印这行文字,否则不会打印这行文字,也不会往下执行')
    userList.value = res.data
  } catch (error) {
    console.log('如果异步失败,会打印这行文字')
    // 由于 axios 在封装的时候,已经在异步失败给了失败的提示弹窗处理
    // 所以这里没必要再写 Message.error(error)
  } finally {
    console.log('如果异步或者失败,都会打印这行代码')
    loading.value = false // 可以用来处理 loading
  }
}
</script>

写法三

需要 loading, 不需要错误打印(不进行错误处理)的情况

<script setup lang="ts">
import { ref } from 'vue'
import { getUserList as getUserListApi, type UserItem } from '@/apis' // 同名可以使用别名

const loading = ref(false)
const userList = ref<UserItem[]>([])
const getUserList = async () => {
  try {
    loading.value = true
    const res = await getUserListApi()
    console.log('如果异步成功,则会打印这行文字,否则不会打印这行文字,也不会往下执行')
    userList.value = res.data
  } finally {
    console.log('如果异步或者失败,都会打印这行代码')
    loading.value = false // 可以用来处理 loading
  }
}
</script>

// catch 可以省略

全局组件--命名规范

组件命名:单文件组件的文件名应该要么始终是单词大写开头 (PascalCase),要么始终是横线连接 (kebab-case)

可参考 Vue2 官网-风格指南: https://v2.cn.vuejs.org/v2/style-guide/

GiForm.vue
GiTable.vue
GiPageLayout.vue

局部组件--命名规范

组件命名:单文件组件的文件名应该要么始终是单词大写开头 (PascalCase),要么始终是横线连接 (kebab-case)

可参考 Vue2 官网-风格指南: https://v2.cn.vuejs.org/v2/style-guide/

Pane1.vue
Pane2.vue
PaneQuota1.vue
PaneQuota2.vue
Step1.vue
Step2.vue
AddModal.vue
EditDrawer.vue
DetailModal.vue

文件夹命名--命名规范 (采用中划线-)

1、文件名建议只使用小写字母,不使用大写字母

2、名称较长时采用半角连接符(-)分隔

home/index.vue
user/index.vue
user-detail/index.vue

弹窗组件 Modal、抽屉组件 Drawer 的一般封装

<template>
  <a-modal v-model:visible="visible" :title="title" @ok="confirm">
    <!-- 内容 -->
  </a-modal>
</template>

<script setup lang="ts">
import { computed, reactive, ref } from 'vue'

const visible = ref(false)
const detailId = ref('')
const isEdit = computed(() => !!detailId.value) // 判断是新增还是编辑模式
const title = computed(() => (isEdit.value ? '编辑' : '新增'))

const add = () => {
  detailId.value = ''
  visible.value = true
}

// 如果这里的参数超过两个,建议优化成对象形式
// const edit = ({ id, taskId }) = {
//   console.log(id, taskId)
// }

const edit = (id: string) => {
  detailId.value = id
  visible.value = true
  // getDetail() 回显操作
}

defineExpose({ add, edit })

const confirm = () => {
  console.log('点击了确认按钮')
}
</script>

使用

模板里使用自定义组件:使用大写开头驼峰,双击好复制,对于搜索便利

<template>
  <EditModal ref="EditModalRef"></EditModal>
</template>

<script setup lang="ts">
import EditModal from './EditModal.vue'

const EditModalRef = ref<InstanceType<typeof EditModal>>()

// 新增
const onAdd = () => {
  EditModalRef.value?.add()
}

// 编辑
const onEdit = (item: PersonItem) => {
  EditModalRef.value?.edit(item.id)
}
</script>

CSS 命名规范

建议采用全小写,多单词使用-连接符(参考大部分网站,包括掘金,码云等,都是采用这个规则)

或者采用BEM命名规范 BEM 命名规范

BEM 命名规范

<div class="article">
  <div class="article__body">
    <button class="article__button--primary"></button>
    <button class="article__button--success"></button>
  </div>
</div>

其他:

位置 1: 使用 var(--margin) 全局 css 变量

位置 2:使用 var(--padding) 全局 css 变量

建议尽量使用全局 css 变量来开发,可以有效提高效率和团队协作

其他规范

可参考 Vue2 官网-风格指南: https://v2.cn.vuejs.org/v2/style-guide/ , 其中一些规范也可借鉴

可参考 Gi Admin Pro 源码,如有更好的规范建议,可以联系作者本人

Vue 相关

Vue3 官网

Vue-Router

Vite

Pinia

插件推荐

Arco Design 组件库

Day.js 一个极简的 JavaScript 库,可以为现代浏览器解析、验证、操作和显示日期和时间 2K 大小

Lodash 一个一致性、模块化、高性能的 JavaScript 实用工具库

Xe-utils 一个 JavaScript 函数库、工具类

VueUse 一个 Vue3 Hooks 库

VueRequest 一个 Vue 请求库

V-Viewer 基于 viewer.js 的 vue 图像查看器组件,支持旋转、缩放等操作

Vue-Color-Kit 一个 vue3 颜色选择器组件

Vxe-Table

vue3-request 一个专为 Vue 3 设计的异步请求处理库,它通过统一的 useRequest Hook 和强大的插件生态,彻底简化了异步操作和 API 调用的复杂性

其他

VCalendar 日历组件

Vue Cal 日历组件

VueDraggablePlus 支持 Vue2 和 Vue3 的拖拽组件

推荐书籍

Vue 入门指南与实战案例

深入理解 TypeScript

前端宝典

Web 开发人员需要知道的 CSS 技巧

阮一峰 ES6

阮一峰 flex 布局

开源项目集合

Vue3 开源项目集合

React 开源项目集合

工具

菜鸟工具

代码生成图片

JSON-TO-ANY 对象转 ts 类型

在线三角形样式生成器

iTab

捐赠