Procházet zdrojové kódy

fix:登陆修改,修复审批没有被指和预约时间的问题

张旭伟 před 5 dny
rodič
revize
4f92e107a7

+ 1 - 0
package.json

@@ -19,6 +19,7 @@
     "await-to-js": "^3.0.0",
     "axios": "^1.8.2",
     "cropperjs": "1.5.13",
+    "crypto-js": "^4.1.1",
     "dayjs": "^1.11.13",
     "downloadjs": "^1.4.7",
     "element-plus": "^2.9.8",

+ 380 - 0
src/utils/aesCrypto.ts

@@ -0,0 +1,380 @@
+import CryptoJS from 'crypto-js'
+
+/**
+ * 增强版AES CFB加密解密工具
+ * 采用CFB模式,无需填充,解决CBC模式填充问题
+ */
+
+// 配置接口
+export interface AESConfig {
+  key: string
+  iv: string
+  mode: typeof CryptoJS.mode.CFB
+  padding: typeof CryptoJS.pad.NoPadding
+}
+
+// 加密结果接口
+export interface EncryptResult {
+  encryptedData: string
+  iv: string
+  timestamp: number
+  version: string
+}
+
+// 后端使用的十六进制密钥和IV(与后端完全匹配)
+const BACKEND_AES_KEY = "e36fa8396463df288532c5f1adc4c9fb960ec8decff0d2856cd94ad340638e6f"
+const BACKEND_AES_IV = "1c16606009b8e3630ddbaeb06c831099"
+
+// 默认配置
+const DEFAULT_CONFIG: AESConfig = {
+  key: BACKEND_AES_KEY,
+  iv: BACKEND_AES_IV,
+  mode: CryptoJS.mode.CFB,
+  padding: CryptoJS.pad.NoPadding
+}
+
+/**
+ * 密钥和IV处理函数
+ * 后端使用十六进制格式,需要将十六进制字符串转换为字节数组
+ */
+function processKeyAndIV(config: AESConfig): { key: CryptoJS.WordArray; iv: CryptoJS.WordArray } {
+  // 检查是否为十六进制字符串(后端格式)
+  const isHexKey = /^[0-9a-fA-F]+$/.test(config.key)
+  const isHexIV = /^[0-9a-fA-F]+$/.test(config.iv)
+
+  if (isHexKey && isHexIV) {
+    // 后端格式:十六进制字符串,直接解析为字节数组
+    return {
+      key: CryptoJS.enc.Hex.parse(config.key),
+      iv: CryptoJS.enc.Hex.parse(config.iv)
+    }
+  } else {
+    // 前端格式:字符串,使用UTF-8编码
+    // 密钥处理:确保32字节长度
+    const processedKey = config.key.padEnd(32, '0').substring(0, 32)
+
+    // IV处理:确保16字节长度
+    const processedIV = config.iv.padEnd(16, '0').substring(0, 16)
+
+    return {
+      key: CryptoJS.enc.Utf8.parse(processedKey),
+      iv: CryptoJS.enc.Utf8.parse(processedIV)
+    }
+  }
+}
+
+/**
+ * 验证配置有效性
+ */
+function validateConfig(config: AESConfig): void {
+  if (!config.key || config.key.trim().length === 0) {
+    throw new Error('AES密钥不能为空')
+  }
+
+  if (!config.iv || config.iv.trim().length === 0) {
+    throw new Error('AES IV不能为空')
+  }
+
+  // 检查是否为十六进制格式
+  const isHexKey = /^[0-9a-fA-F]+$/.test(config.key)
+  const isHexIV = /^[0-9a-fA-F]+$/.test(config.iv)
+
+  if (isHexKey && isHexIV) {
+    // 十六进制格式:密钥应为64字符(32字节),IV应为32字符(16字节)
+    if (config.key.length !== 64) {
+      throw new Error('十六进制AES密钥长度必须为64字符(32字节)')
+    }
+
+    if (config.iv.length !== 32) {
+      throw new Error('十六进制AES IV长度必须为32字符(16字节)')
+    }
+  } else {
+    // 字符串格式:长度要求
+    if (config.key.length < 16) {
+      throw new Error('AES密钥长度至少16位')
+    }
+
+    if (config.iv.length < 16) {
+      throw new Error('AES IV长度至少16位')
+    }
+  }
+}
+
+/**
+ * 增强版AES加密函数
+ * @param data 需要加密的数据
+ * @param customConfig 自定义配置(可选)
+ * @returns 加密结果对象
+ */
+export function enhancedEncrypt(data: string | object, customConfig?: Partial<AESConfig>): EncryptResult {
+  try {
+    if (!data) {
+      throw new Error('加密数据不能为空')
+    }
+
+    // 合并配置
+    const config: AESConfig = { ...DEFAULT_CONFIG, ...customConfig }
+
+    // 验证配置
+    validateConfig(config)
+
+    // 处理密钥和IV
+    const { key, iv } = processKeyAndIV(config)
+
+    // 准备明文数据
+    const plaintext = typeof data === 'object' ? JSON.stringify(data) : String(data)
+
+    // 对数据进行UTF-8编码
+    const srcs = CryptoJS.enc.Utf8.parse(plaintext)
+
+    // 执行AES CFB加密
+    const encrypted = CryptoJS.AES.encrypt(srcs, key, {
+      iv: iv,
+      mode: config.mode,
+      padding: config.padding
+    })
+
+    // 返回完整的加密结果
+    return {
+      encryptedData: CryptoJS.enc.Base64.stringify(encrypted.ciphertext),
+      iv: CryptoJS.enc.Base64.stringify(iv),
+      timestamp: Date.now(),
+      version: '3.0' // 更新版本号以标识CFB模式
+    }
+
+  } catch (error) {
+    console.error('AES加密失败:', error)
+    throw new Error(`加密失败: ${error instanceof Error ? error.message : '未知错误'}`)
+  }
+}
+
+/**
+ * 增强版AES解密函数
+ * @param encryptedData 加密的Base64字符串
+ * @param iv IV的Base64字符串
+ * @param customConfig 自定义配置(可选)
+ * @returns 解密后的原始数据
+ */
+export function enhancedDecrypt(encryptedData: string, iv: string, customConfig?: Partial<AESConfig>): string | object {
+  try {
+    if (!encryptedData || !iv) {
+      throw new Error('解密数据或IV不能为空')
+    }
+
+    // 合并配置
+    const config: AESConfig = { ...DEFAULT_CONFIG, ...customConfig }
+
+    // 验证配置
+    validateConfig(config)
+
+    // 处理密钥
+    const { key } = processKeyAndIV(config)
+
+    // 解析IV
+    const aesIv = CryptoJS.enc.Base64.parse(iv)
+
+    // 执行AES CFB解密
+    const decrypt = CryptoJS.AES.decrypt(encryptedData, key, {
+      iv: aesIv,
+      mode: config.mode,
+      padding: config.padding
+    })
+
+    // 转换为UTF-8字符串
+    const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8)
+
+    if (!decryptedStr) {
+      throw new Error('解密失败,可能是密钥错误或数据损坏')
+    }
+
+    // 尝试解析为JSON对象,如果不是JSON则返回字符串
+    try {
+      return JSON.parse(decryptedStr)
+    } catch {
+      return decryptedStr
+    }
+
+  } catch (error) {
+    console.error('AES解密失败:', error)
+    throw new Error(`解密失败: ${error instanceof Error ? error.message : '未知错误'}`)
+  }
+}
+
+/**
+ * 加密敏感数据(业务专用)
+ * @param sensitiveData 敏感数据对象
+ * @returns 加密结果
+ */
+export function encryptSensitiveData(sensitiveData: object): EncryptResult {
+  const dataToEncrypt = {
+    ...sensitiveData,
+    encryptedAt: new Date().toISOString(),
+    version: '3.0',
+    securityLevel: 'high'
+  }
+
+  return enhancedEncrypt(dataToEncrypt)
+}
+
+/**
+ * 与后端完全匹配的加密函数
+ * 使用后端相同的十六进制密钥和IV,确保加密解密完全匹配
+ * @param plaintext 明文数据
+ * @returns 加密后的Base64字符串
+ */
+export function encryptWithBackendConfig(plaintext: string): string {
+  try {
+    if (!plaintext) {
+      throw new Error('加密数据不能为空')
+    }
+
+    // 使用后端配置
+    const config: AESConfig = { ...DEFAULT_CONFIG }
+
+    // 验证配置
+    validateConfig(config)
+
+    // 处理密钥和IV(十六进制格式)
+    const { key, iv } = processKeyAndIV(config)
+
+    // 对数据进行UTF-8编码
+    const srcs = CryptoJS.enc.Utf8.parse(plaintext)
+
+    // 执行AES CFB加密
+    const encrypted = CryptoJS.AES.encrypt(srcs, key, {
+      iv: iv,
+      mode: config.mode,
+      padding: config.padding
+    })
+
+    // 返回Base64编码的加密数据
+    return CryptoJS.enc.Base64.stringify(encrypted.ciphertext)
+
+  } catch (error) {
+    console.error('后端匹配加密失败:', error)
+    throw new Error(`加密失败: ${error instanceof Error ? error.message : '未知错误'}`)
+  }
+}
+
+/**
+ * 与后端完全匹配的解密函数
+ * @param encryptedData 加密的Base64字符串
+ * @returns 解密后的原始数据
+ */
+export function decryptWithBackendConfig(encryptedData: string): string {
+  try {
+    if (!encryptedData) {
+      throw new Error('解密数据不能为空')
+    }
+
+    // 使用后端配置
+    const config: AESConfig = { ...DEFAULT_CONFIG }
+
+    // 验证配置
+    validateConfig(config)
+
+    // 处理密钥
+    const { key, iv } = processKeyAndIV(config)
+
+    // 执行AES CFB解密
+    const decrypt = CryptoJS.AES.decrypt(encryptedData, key, {
+      iv: iv,
+      mode: config.mode,
+      padding: config.padding
+    })
+
+    // 转换为UTF-8字符串
+    const decryptedStr = decrypt.toString(CryptoJS.enc.Utf8)
+
+    if (!decryptedStr) {
+      throw new Error('解密失败,可能是密钥错误或数据损坏')
+    }
+
+    return decryptedStr
+
+  } catch (error) {
+    console.error('后端匹配解密失败:', error)
+    throw new Error(`解密失败: ${error instanceof Error ? error.message : '未知错误'}`)
+  }
+}
+
+/**
+ * 解密登录数据
+ * @param loginData 登录数据对象
+ * @returns 解密后的登录信息
+ */
+export function decryptLoginData(loginData: { aesPassword: string; saltValue: string }): any {
+  try {
+    // 使用saltValue作为IV进行解密
+    return enhancedDecrypt(loginData.aesPassword, loginData.saltValue)
+  } catch (error) {
+    console.error('登录数据解密失败:', error)
+    throw error
+  }
+}
+
+/**
+ * 验证加密数据完整性
+ * @param encryptResult 加密结果
+ * @returns 是否有效
+ */
+export function validateEncryptedData(encryptResult: EncryptResult): boolean {
+  try {
+    if (!encryptResult.encryptedData || !encryptResult.iv) {
+      return false
+    }
+
+    // 检查时间戳是否合理(不超过24小时)
+    const now = Date.now()
+    const timeDiff = now - encryptResult.timestamp
+    if (timeDiff > 24 * 60 * 60 * 1000) {
+      return false
+    }
+
+    // 检查版本号(支持2.0和3.0版本)
+    if (!encryptResult.version || (encryptResult.version !== '2.0' && encryptResult.version !== '3.0')) {
+      return false
+    }
+
+    return true
+  } catch {
+    return false
+  }
+}
+
+/**
+ * 生成安全的随机密钥
+ * @param length 密钥长度
+ * @returns 随机密钥
+ */
+export function generateSecureKey(length: number = 32): string {
+  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?'
+  const crypto = window.crypto || (window as any).msCrypto
+
+  if (crypto && crypto.getRandomValues) {
+    // 使用Web Crypto API生成更安全的随机数
+    const array = new Uint32Array(length)
+    crypto.getRandomValues(array)
+    let result = ''
+    for (let i = 0; i < length; i++) {
+      result += chars[array[i] % chars.length]
+    }
+    return result
+  } else {
+    // 回退方案
+    let result = ''
+    for (let i = 0; i < length; i++) {
+      result += chars.charAt(Math.floor(Math.random() * chars.length))
+    }
+    return result
+  }
+}
+
+export default {
+  enhancedEncrypt,
+  enhancedDecrypt,
+  encryptSensitiveData,
+  decryptLoginData,
+  validateEncryptedData,
+  generateSecureKey
+}

+ 5 - 0
src/view/login/index.vue

@@ -76,6 +76,7 @@ import { storeToRefs } from 'pinia'
 import { useUserInfo } from '/@/stores/userInfo'
 import { showDialog, showToast } from 'vant'
 import { HttpStatus } from '/@/constants/pageConstants'
+import { enhancedEncrypt, decryptLoginData, encryptWithBackendConfig } from '/@/utils/aesCrypto';
 
 const router = useRouter()
 const route = useRoute()
@@ -124,6 +125,7 @@ const state = reactive({
     password: '',
     idValueC: '',
     idKeyC: '',
+    saltValue: '',
   },
 })
 
@@ -144,6 +146,9 @@ const onSignIn = async () => {
   params.unionId = unionId.value
   // params.password = (params.password)
   // sm3
+  // 使用与后端完全匹配的加密函数加密密码
+  const encryptedPassword = encryptWithBackendConfig(params.password);
+  params.saltValue = encryptedPassword; // 后端AES IV常量
   const post = params.openId ? loginApi.weChatLogin : loginApi.signIn
   const [err, res]: ToResponse = await to(post(params))
   state.loading.signIn = false

+ 59 - 1
src/view/todo/component/instrument_appointment.vue

@@ -6,6 +6,17 @@
       <van-cell title="仪器类型" title-class="cell-title" :value="state.form.instClassDesc" />
       <van-cell title="计费方式" title-class="cell-title" :value="state.form.costType" />
       <van-cell title="预约人" title-class="cell-title" :value="state.form.userName" />
+      <van-cell title="预约时间段" title-class="cell-title">
+        <template #value>
+          <div class="time-range">
+            <div class="time-item">{{ state.form.startTime }}</div>
+            <div class="time-connector">至</div>
+            <div class="time-item">{{ state.form.endTime }}</div>
+          </div>
+        </template>
+      </van-cell>
+      <van-cell title="辅助上机" title-class="cell-title" :value="state.form.assistEnable ? '是' : '否'" />
+      <van-cell title="备注" title-class="cell-title" :value="state.form.remark" />
     </van-cell-group>
   </div>
 </template>
@@ -37,6 +48,10 @@
       costType: '',
       appointeeInstrumentName: '',
       userName: '',
+      startTime: '',
+      endTime: '',
+      assistEnable: false,
+      remark: '',
     },
   })
 
@@ -67,4 +82,47 @@
     display: flex;
     justify-content: space-between;
   }
-</style>
+  
+  .time-range {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    height: 100%;
+    gap: 8px;
+  }
+  
+  .time-item {
+    flex: 1;
+    text-align: center;
+    min-width: 0;
+  }
+  
+  .time-connector {
+    color: #969799;
+    font-size: 12px;
+    position: relative;
+    padding: 0 4px;
+  }
+  
+  .time-connector::before {
+    content: '';
+    position: absolute;
+    top: 50%;
+    left: 0;
+    right: 0;
+    height: 1px;
+    background-color: #ebedf0;
+    transform: translateY(-50%);
+  }
+  
+  .time-connector::after {
+    content: '';
+    position: absolute;
+    top: 50%;
+    left: 0;
+    right: 0;
+    height: 1px;
+    background-color: #ebedf0;
+    transform: translateY(-50%);
+  }
+</style>