手把手教你打造基于RuoYi-Vue-Plus框架的考勤管理系统(二)

一、摘要

本文将以前文《手把手教你打造基于RuoYi-Vue-Plus框架的考勤管理系统》所奠定的坚实基础为出发点,进一步深入探索并完善该考勤管理系统的两大核心功能模块------考勤统计与考勤明细,同时拓展系统边界,延伸至移动端应用,特别是原生小程序端的开发,旨在为用户提供一个更加全面、便捷且高效的考勤管理体验。

文章编写不易,大家给点点着支持支持!!!

二、完善考勤明细

在考勤明细功能如下图所示:

考勤明细就是需要我们根据时间戳范围获取考勤记录,并根据进出闭合计算出工作时长。未打卡的显示 "__",已打卡但是没有闭合的显示0.00.

废话不多说,直接上代码:

java 复制代码
    /**
     * 获取考勤统计列表
     *
     * @param bo
     * @param pageQuery
     * @return
     */
    @SaCheckPermission("mqtt:recordCount:list")
    @GetMapping("/list")
    public TableDataInfo<RecordDetails> list(RecordDetailsBo bo, PageQuery pageQuery) {
        return recordDetailsService.queryPageCountList(bo, pageQuery);
    }

/** 接口实现类方法 */
 @Override
    public TableDataInfo<RecordDetails> queryPageCountList(RecordDetailsBo bo, PageQuery pageQuery) {
        Map<String, Object> params = bo.getParams();
        LambdaQueryChainWrapper<RecordDetails> lwq = this.lambdaQuery();
        lwq.eq(RecordDetails::getTenantId, LoginHelper.getTenantId());
        lwq.like(StringUtils.isNoneBlank(bo.getName()), RecordDetails::getName, bo.getName());
        PageResult<RecordDetails> page = this.page(lwq, pageQuery.getPageNum(), pageQuery.getPageSize());
        for (RecordDetails recordDetails : page.getContentData()) {
            String wrkeId = recordDetails.getWorkeId().toString();
            Map<String, Map<String, String>> mapMap = recordServiceApi.getRecordDaysCountByPersonId(wrkeId, bo.getBeginCreateTime(), bo.getEndCreateTime());
            recordDetails.setRecordDaysCounts(mapMap);
        }
        TableDataInfo<RecordDetails> tableDataInfo = new TableDataInfo<>();
        tableDataInfo.setTotal(page.getTotalSize());
        tableDataInfo.setRows(page.getContentData());
        tableDataInfo.setMsg("查询成功");
        tableDataInfo.setCode(HttpStatus.HTTP_OK);
        return tableDataInfo;
    }

/** recordServiceApi.getRecordDaysCountByPersonId 方法代码 */
    public Map<String, Map<String, String>> getRecordDaysCountByPersonId(String wrkeId, Long beginCreateTime, Long endCreateTime) {
        LambdaQueryChainWrapper<Record> lwq = this.lambdaQuery();
        lwq.eq(Record::getPersonId, wrkeId);
        lwq.eq(Record::getTenantId, LoginHelper.getTenantId());
        lwq.between(true, Record::getCreatedAt, beginCreateTime, endCreateTime, false);
        List<Record> list = lwq.list();
        LocalDate startDate = Instant.ofEpochMilli(beginCreateTime).atZone(ZoneId.of("Asia/Shanghai")).toLocalDate();
        LocalDate endDate = Instant.ofEpochMilli(endCreateTime).atZone(ZoneId.of("Asia/Shanghai")).toLocalDate();
        List<LocalDate> dateRange = new ArrayList<>();
        for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
            dateRange.add(date);
        }
        Map<String, Map<String, String>> monthlyResult = new LinkedHashMap<>();
        for (LocalDate date : dateRange) {
            String monthKey = date.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM"));
            monthlyResult.putIfAbsent(monthKey, new LinkedHashMap<>());
            String dayOfMonth = String.format("%02d", date.getDayOfMonth());
            monthlyResult.get(monthKey).put(dayOfMonth, "--");
        }
        if (CollUtil.isNotEmpty(list)) {
            TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            sdf.setTimeZone(timeZone);
            Map<String, Map<String, List<Record>>> groupedByYearMonthDay = list.stream()
                .collect(Collectors.groupingBy(record -> {
                    LocalDate date = LocalDate.ofInstant(
                        java.time.Instant.ofEpochMilli(record.getCreatedAt()),
                        ZoneId.systemDefault()
                    );
                    return date.getYear() + "-" + String.format("%02d", date.getMonthValue());
                }, Collectors.groupingBy(record -> {
                    LocalDate date = LocalDate.ofInstant(
                        java.time.Instant.ofEpochMilli(record.getCreatedAt()),
                        ZoneId.systemDefault()
                    );
                    return String.format("%02d", date.getDayOfMonth());
                })));
            Map<String, String> dailyWorkHours = new HashMap<>();
            for (Map.Entry<String, Map<String, List<Record>>> yearMonthEntry : groupedByYearMonthDay.entrySet()) {
                String yearMonth = yearMonthEntry.getKey();
                Map<String, List<Record>> dayMap = yearMonthEntry.getValue();
                for (Map.Entry<String, List<Record>> dayEntry : dayMap.entrySet()) {
                    String day = dayEntry.getKey();
                    List<Record> records = dayEntry.getValue();
                    long totalDuration = 0;
                    long prevTime = -1;
                    for (Record record : records) {
                        long currentTime = record.getCreatedAt();
                        if (prevTime != -1 && "1".equals(record.getLockInOutStatus())) {
                            totalDuration += currentTime - prevTime;
                            prevTime = -1;
                        }
                        if ("0".equals(record.getLockInOutStatus())) {
                            prevTime = currentTime;
                        }
                        double hours = totalDuration / (1000.0 * 60 * 60);
                        String fullDate = yearMonth + "-" + day;
                        dailyWorkHours.put(fullDate, String.format("%.2f", hours) + "小时");
                    }
                }
            }
            for (Map.Entry<String, Map<String, String>> monthEntry : monthlyResult.entrySet()) {
                String monthKey = monthEntry.getKey();
                Map<String, String> daysMap = monthEntry.getValue();
                for (Map.Entry<String, String> dayEntry : daysMap.entrySet()) {
                    String dayOfMonth = dayEntry.getKey();
                    String fullDate = monthKey + "-" + dayOfMonth;
                    if (dailyWorkHours.containsKey(fullDate)) {
                        daysMap.put(dayOfMonth, dailyWorkHours.get(fullDate));
                    }
                }
            }
        }
        return monthlyResult;
    }

到这里我们的获取考勤明细功能就完善了!!我这里是新建了一个表,如果大家基于ruoyi-vue-plus 可以直接用user表然后添加对应字段/或建立中间表即可。

三、完善考勤统计功能

考勤统计和考勤明细我使用的是同一张表所以看起来有点怪,大家不要介意哈!!!

考勤统计是要计算出每一天每一天的工作时间,然后渲染给前端显示。如下图所示:

代码:

java 复制代码
    /**
     * 获取考勤统计列表
     *
     * @param bo
     * @param pageQuery
     * @return
     */
    @SaCheckPermission("mqtt:recordCount:list")
    @GetMapping("/list")
    public TableDataInfo<RecordDetails> list(RecordDetailsBo bo, PageQuery pageQuery) {
        return recordDetailsService.queryPageCountList(bo, pageQuery);
    }

/** 接口实现类方法 */
    @Override
    public TableDataInfo<RecordDetails> queryPageCountList(RecordDetailsBo bo, PageQuery pageQuery) {
        Map<String, Object> params = bo.getParams();
        LambdaQueryChainWrapper<RecordDetails> lwq = this.lambdaQuery();
        lwq.eq(RecordDetails::getTenantId, LoginHelper.getTenantId());
        lwq.like(StringUtils.isNoneBlank(bo.getName()), RecordDetails::getName, bo.getName());
        PageResult<RecordDetails> page = this.page(lwq, pageQuery.getPageNum(), pageQuery.getPageSize());
        for (RecordDetails recordDetails : page.getContentData()) {
            String wrkeId = recordDetails.getWorkeId().toString();
            Map<String, Map<String, String>> mapMap = recordServiceApi.getRecordDaysCountByPersonId(wrkeId, bo.getBeginCreateTime(), bo.getEndCreateTime());
            recordDetails.setRecordDaysCounts(mapMap);
        }
        TableDataInfo<RecordDetails> tableDataInfo = new TableDataInfo<>();
        tableDataInfo.setTotal(page.getTotalSize());
        tableDataInfo.setRows(page.getContentData());
        tableDataInfo.setMsg("查询成功");
        tableDataInfo.setCode(HttpStatus.HTTP_OK);
        return tableDataInfo;
    }

/** recordServiceApi.getRecordDaysCountByPersonId方法 */
    @Override
    public Map<String, Map<String, String>> getRecordDaysCountByPersonId(String wrkeId, Long beginCreateTime, Long endCreateTime) {
        LambdaQueryChainWrapper<Record> lwq = this.lambdaQuery();
        lwq.eq(Record::getPersonId, wrkeId);
        lwq.eq(Record::getTenantId, LoginHelper.getTenantId());
        lwq.between(true, Record::getCreatedAt, beginCreateTime, endCreateTime, false);
        List<Record> list = lwq.list();
        LocalDate startDate = Instant.ofEpochMilli(beginCreateTime).atZone(ZoneId.of("Asia/Shanghai")).toLocalDate();
        LocalDate endDate = Instant.ofEpochMilli(endCreateTime).atZone(ZoneId.of("Asia/Shanghai")).toLocalDate();
        List<LocalDate> dateRange = new ArrayList<>();
        for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
            dateRange.add(date);
        }
        Map<String, Map<String, String>> monthlyResult = new LinkedHashMap<>();
        for (LocalDate date : dateRange) {
            String monthKey = date.format(java.time.format.DateTimeFormatter.ofPattern("yyyy-MM"));
            monthlyResult.putIfAbsent(monthKey, new LinkedHashMap<>());
            String dayOfMonth = String.format("%02d", date.getDayOfMonth());
            monthlyResult.get(monthKey).put(dayOfMonth, "--");
        }
        if (CollUtil.isNotEmpty(list)) {
            TimeZone timeZone = TimeZone.getTimeZone("Asia/Shanghai");
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            sdf.setTimeZone(timeZone);
            Map<String, Map<String, List<Record>>> groupedByYearMonthDay = list.stream()
                .collect(Collectors.groupingBy(record -> {
                    LocalDate date = LocalDate.ofInstant(
                        java.time.Instant.ofEpochMilli(record.getCreatedAt()),
                        ZoneId.systemDefault()
                    );
                    return date.getYear() + "-" + String.format("%02d", date.getMonthValue());
                }, Collectors.groupingBy(record -> {
                    LocalDate date = LocalDate.ofInstant(
                        java.time.Instant.ofEpochMilli(record.getCreatedAt()),
                        ZoneId.systemDefault()
                    );
                    return String.format("%02d", date.getDayOfMonth());
                })));
            Map<String, String> dailyWorkHours = new HashMap<>();
            for (Map.Entry<String, Map<String, List<Record>>> yearMonthEntry : groupedByYearMonthDay.entrySet()) {
                String yearMonth = yearMonthEntry.getKey();
                Map<String, List<Record>> dayMap = yearMonthEntry.getValue();
                for (Map.Entry<String, List<Record>> dayEntry : dayMap.entrySet()) {
                    String day = dayEntry.getKey();
                    List<Record> records = dayEntry.getValue();
                    long totalDuration = 0;
                    long prevTime = -1;
                    for (Record record : records) {
                        long currentTime = record.getCreatedAt();
                        if (prevTime != -1 && "1".equals(record.getLockInOutStatus())) {
                            totalDuration += currentTime - prevTime;
                            prevTime = -1;
                        }
                        if ("0".equals(record.getLockInOutStatus())) {
                            prevTime = currentTime;
                        }
                        double hours = totalDuration / (1000.0 * 60 * 60);
                        String fullDate = yearMonth + "-" + day;
                        dailyWorkHours.put(fullDate, String.format("%.2f", hours) + "小时");
                    }
                }
            }
            for (Map.Entry<String, Map<String, String>> monthEntry : monthlyResult.entrySet()) {
                String monthKey = monthEntry.getKey();
                Map<String, String> daysMap = monthEntry.getValue();
                for (Map.Entry<String, String> dayEntry : daysMap.entrySet()) {
                    String dayOfMonth = dayEntry.getKey();
                    String fullDate = monthKey + "-" + dayOfMonth;
                    if (dailyWorkHours.containsKey(fullDate)) {
                        daysMap.put(dayOfMonth, dailyWorkHours.get(fullDate));
                    }
                }
            }
        }
        return monthlyResult;
    }

vue页面动态渲染的嵌套表头和表格内容代码

html 复制代码
        <template v-if="recordDetailsList.length > 0" v-for="(month, monthKey) in getRecordDaysCountsKeys(recordDetailsList[0])" :key="monthKey">
          <el-table-column :label="month" align="center">
            <el-table-column
                v-for="(day, dayKey) in getDays(recordDetailsList[0], month)"
                :key="dayKey"
                :label="dayKey"
                align="center"
            >
              <template #default="scope">
                <el-link disabled v-if="getDayValue(scope.row, month, dayKey) === '--'" >{{ getDayValue(scope.row, month, dayKey) }}</el-link>
                <el-link type="primary" v-else @click="seeAttendance(scope.row, month, dayKey)">{{ getDayValue(scope.row, month, dayKey) }}</el-link>
              </template>
            </el-table-column>
          </el-table-column>
        </template>

到这里我们就完善了考勤统计功能!!!

如果大家需要写导出功能,可以参考文章 《EasyExcel自定义导出实现》

四、小程序端

1.开发环境

2.新建小程序项目

勾选不使用云服务 + TS Sass 基础模板即可。如图所示:

3.编写api请求加密功能

ps:微信小程序不支持JSEncryptLib,网上有方法可以解决。但是我选择了WxmpRsa去替代,大家按需使用即可。安装tdesign的步骤我不在描述了,网上的教程很多QWQ

auth.ts

javascript 复制代码
const TokenKey = 'Admin-Token';

// 获取 Token
export const getToken = () => {
  try {
    return wx.getStorageSync(TokenKey) || null; // 如果缓存中没有值,返回 null
  } catch (error) {
    console.error('获取 Token 失败:', error);
    return null;
  }
};

// 设置 Token
export const setToken = (access_token:string) => {
  try {
    wx.setStorageSync(TokenKey, access_token); // 将 Token 存入缓存
  } catch (error) {
    console.error('设置 Token 失败:', error);
  }
};

// 移除 Token
export const removeToken = () => {
  try {
    wx.removeStorageSync(TokenKey); // 从缓存中移除 Token
  } catch (error) {
    console.error('移除 Token 失败:', error);
  }
};

ruoyi.ts

javascript 复制代码
// 日期格式化
export function parseTime(time: any, pattern?: string) {
  if (arguments.length === 0 || !time) {
    return null;
  }
  const format = pattern || '{y}-{m}-{d} {h}:{i}:{s}';
  let date;
  if (typeof time === 'object') {
    date = time;
  } else {
    if (typeof time === 'string' && /^[0-9]+$/.test(time)) {
      time = parseInt(time);
    } else if (typeof time === 'string') {
      time = time
        .replace(new RegExp(/-/gm), '/')
        .replace('T', ' ')
        .replace(new RegExp(/\.[\d]{3}/gm), '');
    }
    if (typeof time === 'number' && time.toString().length === 10) {
      time = time * 1000;
    }
    date = new Date(time);
  }
  const formatObj: { [key: string]: any } = {
    y: date.getFullYear(),
    m: date.getMonth() + 1,
    d: date.getDate(),
    h: date.getHours(),
    i: date.getMinutes(),
    s: date.getSeconds(),
    a: date.getDay()
  };
  return format.replace(/{(y|m|d|h|i|s|a)+}/g, (result: string, key: string) => {
    let value = formatObj[key];
    // Note: getDay() returns 0 on Sunday
    if (key === 'a') {
      return ['日', '一', '二', '三', '四', '五', '六'][value];
    }
    if (result.length > 0 && value < 10) {
      value = '0' + value;
    }
    return value || 0;
  });
}

/**
 * 添加日期范围
 * @param params
 * @param dateRange
 * @param propName
 */
export const addDateRange = (params: any, dateRange: any[], propName?: string) => {
  const search = params;
  search.params = typeof search.params === 'object' && search.params !== null && !Array.isArray(search.params) ? search.params : {};
  dateRange = Array.isArray(dateRange) ? dateRange : [];
  if (typeof propName === 'undefined') {
    search.params['beginTime'] = dateRange[0];
    search.params['endTime'] = dateRange[1];
  } else {
    search.params['begin' + propName] = dateRange[0];
    search.params['end' + propName] = dateRange[1];
  }
  return search;
};

// 回显数据字典
export const selectDictLabel = (datas: any, value: number | string) => {
  if (value === undefined) {
    return '';
  }
  const actions: Array<string | number> = [];
  Object.keys(datas).some((key) => {
    if (datas[key].value == '' + value) {
      actions.push(datas[key].label);
      return true;
    }
  });
  if (actions.length === 0) {
    actions.push(value);
  }
  return actions.join('');
};

// 回显数据字典(字符串数组)
export const selectDictLabels = (datas: any, value: any, separator: any) => {
  if (value === undefined || value.length === 0) {
    return '';
  }
  if (Array.isArray(value)) {
    value = value.join(',');
  }
  const actions: any[] = [];
  const currentSeparator = undefined === separator ? ',' : separator;
  const temp = value.split(currentSeparator);
  Object.keys(value.split(currentSeparator)).some((val) => {
    let match = false;
    Object.keys(datas).some((key) => {
      if (datas[key].value == '' + temp[val]) {
        actions.push(datas[key].label + currentSeparator);
        match = true;
      }
    });
    if (!match) {
      actions.push(temp[val] + currentSeparator);
    }
  });
  return actions.join('').substring(0, actions.join('').length - 1);
};

// 字符串格式化(%s )
export function sprintf(str: string) {
  if (arguments.length !== 0) {
    let flag = true,
      i = 1;
    str = str.replace(/%s/g, function () {
      const arg = arguments[i++];
      if (typeof arg === 'undefined') {
        flag = false;
        return '';
      }
      return arg;
    });
    return flag ? str : '';
  }
}

// 转换字符串,undefined,null等转化为""
export const parseStrEmpty = (str: any) => {
  if (!str || str == 'undefined' || str == 'null') {
    return '';
  }
  return str;
};

// 数据合并
export const mergeRecursive = (source: any, target: any) => {
  for (const p in target) {
    try {
      if (target[p].constructor == Object) {
        source[p] = mergeRecursive(source[p], target[p]);
      } else {
        source[p] = target[p];
      }
    } catch (e) {
      source[p] = target[p];
    }
  }
  return source;
};

/**
 * 构造树型结构数据
 * @param {*} data 数据源
 * @param {*} id id字段 默认 'id'
 * @param {*} parentId 父节点字段 默认 'parentId'
 * @param {*} children 孩子节点字段 默认 'children'
 */
export const handleTree = <T>(data: any[], id?: string, parentId?: string, children?: string): T[] => {
  const config: {
    id: string;
    parentId: string;
    childrenList: string;
  } = {
    id: id || 'id',
    parentId: parentId || 'parentId',
    childrenList: children || 'children'
  };

  const childrenListMap: any = {};
  const nodeIds: any = {};
  const tree: T[] = [];

  for (const d of data) {
    const parentId = d[config.parentId];
    if (childrenListMap[parentId] == null) {
      childrenListMap[parentId] = [];
    }
    nodeIds[d[config.id]] = d;
    childrenListMap[parentId].push(d);
  }

  for (const d of data) {
    const parentId = d[config.parentId];
    if (nodeIds[parentId] == null) {
      tree.push(d);
    }
  }
  const adaptToChildrenList = (o: any) => {
    if (childrenListMap[o[config.id]] !== null) {
      o[config.childrenList] = childrenListMap[o[config.id]];
    }
    if (o[config.childrenList]) {
      for (const c of o[config.childrenList]) {
        adaptToChildrenList(c);
      }
    }
  };

  for (const t of tree) {
    adaptToChildrenList(t);
  }

  return tree;
};

/**
 * 参数处理
 * @param {*} params  参数
 */
export const tansParams = (params: any) => {
  let result = '';
  for (const propName of Object.keys(params)) {
    const value = params[propName];
    const part = encodeURIComponent(propName) + '=';
    if (value !== null && value !== '' && typeof value !== 'undefined') {
      if (typeof value === 'object') {
        for (const key of Object.keys(value)) {
          if (value[key] !== null && value[key] !== '' && typeof value[key] !== 'undefined') {
            const params = propName + '[' + key + ']';
            const subPart = encodeURIComponent(params) + '=';
            result += subPart + encodeURIComponent(value[key]) + '&';
          }
        }
      } else {
        result += part + encodeURIComponent(value) + '&';
      }
    }
  }
  return result;
};

// 返回项目路径
export const getNormalPath = (p: string): string => {
  if (p.length === 0 || !p || p === 'undefined') {
    return p;
  }
  const res = p.replace('//', '/');
  if (res[res.length - 1] === '/') {
    return res.slice(0, res.length - 1);
  }
  return res;
};

// 验证是否为blob格式
export const blobValidate = (data: any) => {
  return data.type !== 'application/json';
};

export default {
  handleTree
};

crypto.ts

javascript 复制代码
import CryptoJS from 'crypto-js';

/**
 * 随机生成32位的字符串
 * @returns {string}
 */
const generateRandomString = (): string => {
  const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let result = '';
  const charactersLength = characters.length;
  for (let i = 0; i < 32; i++) {
    result += characters.charAt(Math.floor(Math.random() * charactersLength));
  }
  return result;
};

/**
 * 随机生成 AES 密钥
 * @returns {CryptoJS.lib.WordArray}
 */
export const generateAesKey = (): CryptoJS.lib.WordArray => {
  return CryptoJS.enc.Utf8.parse(generateRandomString());
};

/**
 * 加密为 Base64 字符串
 * @param {CryptoJS.lib.WordArray} str - 要加密的内容
 * @returns {string}
 */
export const encryptBase64 = (str: CryptoJS.lib.WordArray): string => {
  return CryptoJS.enc.Base64.stringify(str);
};

/**
 * 解密 Base64 字符串
 * @param {string} str - Base64 字符串
 * @returns {CryptoJS.lib.WordArray}
 */
export const decryptBase64 = (str: string): CryptoJS.lib.WordArray => {
  return CryptoJS.enc.Base64.parse(str);
};

/**
 * 使用密钥对数据进行 AES 加密
 * @param {string} message - 要加密的消息
 * @param {CryptoJS.lib.WordArray} aesKey - AES 密钥
 * @returns {string}
 */
export const encryptWithAes = (message: string, aesKey: CryptoJS.lib.WordArray): string => {
  const encrypted = CryptoJS.AES.encrypt(message, aesKey, {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7
  });
  return encrypted.toString();
};

/**
 * 使用密钥对数据进行 AES 解密
 * @param {string} message - 要解密的消息
 * @param {CryptoJS.lib.WordArray} aesKey - AES 密钥
 * @returns {string}
 */
export const decryptWithAes = (message: string, aesKey: CryptoJS.lib.WordArray): string => {
  const decrypted = CryptoJS.AES.decrypt(message, aesKey, {
    mode: CryptoJS.mode.ECB,
    padding: CryptoJS.pad.Pkcs7
  });
  return decrypted.toString(CryptoJS.enc.Utf8);
};

jsencrypt.ts

javascript 复制代码
import WxmpRsa from 'wxmp-rsa';

const publicKey = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdHnzkXSOVOZbFu/TJhZ7rFAN+eaGkl3C4buccQd/EjEsj9ir7ijT7h96MCAwEAAQ==";

// 前端不建议存放私钥 不建议解密数据 因为都是透明的意义不大
const privateKey = "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE=";

// 加密
export const encrypt = (txt: string) => {
  const rsa = new WxmpRsa()
  rsa.setPublicKey(publicKey)
  return rsa.encryptLong(txt);
};

// 解密
export const decrypt = (txt: string) => {
  const rsa = new WxmpRsa()
  rsa.setPrivateKey(privateKey)
  return rsa.decryptLong(txt);
};

config.ts

javascript 复制代码
/**
 * 全局配置文件
 */
export const AppConfig = {
  // clientId 
  VITE_APP_CLIENT_ID: '428a8310cd442757ae699df5d894f051',
  // 请求路径
  BASE_URL: 'http://127.0.0.1:8080'
}

request.ts

javascript 复制代码
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE'] as const;
type HttpMethod = typeof HTTP_METHODS[number]

const encryptHeader = "Encrypt-key";

// 防止重复弹出登录过期提示
const isRelogin = { show: false };

// 全局请求头配置
const globalHeaders = () => {
  return {
    Authorization: 'Bearer ' + getToken(),
    Clientid: AppConfig.VITE_APP_CLIENT_ID
  };
};

/**
 * 发起网络请求
 */
const request = <T = any>(options: {
  url: string;
  method?: HttpMethod;
  data?: any;
  headers?: Record<string, Object>;
  isToken?: boolean; // 是否需要携带 Token
  repeatSubmit?: boolean; // 是否允许重复提交
  isencrypt?: boolean; // 是否需要加密
  timeout?: number; // 超时时间
}): Promise<T> => {
  const {
    url,
    method = 'GET',
    headers = {},
    isToken = true,
    repeatSubmit = false,
    timeout = 50000,
    isencrypt = true
  } = options;
  let data = options.data || {};
  // 构建默认请求头
  let defaultHeaders: Record<string, Object> = {
    'Content-Type': 'application/json;charset=utf-8',
    ...globalHeaders(),
    ...headers
  };
  // 请求头添加token
  if (getToken() && isToken) {
    defaultHeaders['Authorization'] = 'Bearer ' + getToken();
  }
  // 判断请求方式
  if (!HTTP_METHODS.includes(method.toUpperCase() as HttpMethod)) {
    wx.showToast({
      title: "无效的请求方法",
      icon: 'none',
      duration: 2000
    });
    throw new Error(`无效的请求方法: ${method}`);
  }
  const finalMethod = method.toUpperCase() as HttpMethod;
  // GET请求映射params参数
  let finalUrl = url;
  if (method.toUpperCase() === 'GET' && Object.keys(data).length > 0) {
    finalUrl += '?' + tansParams(data);
  }
  // 防止重复提交
  if (!repeatSubmit && (method.toUpperCase() === 'POST' || method.toUpperCase() === 'PUT')) { }
  // 参数加密
  if (isencrypt && (method.toUpperCase() === 'POST' || method.toUpperCase() === 'PUT')) {
    const aesKey = generateAesKey();
    defaultHeaders[encryptHeader] = encrypt(encryptBase64(aesKey)) as string;
    data = typeof data === 'object' ? encryptWithAes(JSON.stringify(data), aesKey) : encryptWithAes(data, aesKey);
  }
  return new Promise((resolve, reject) => {
    wx.request({
      url: AppConfig.BASE_URL + finalUrl,
      method: finalMethod,
      data: data,
      header: defaultHeaders,
      // 超时时间
      timeout: timeout, 
      success: (res: WechatMiniprogram.RequestSuccessCallbackResult) => {
        let { statusCode, data: responseData } = res;
        // 解密响应数据
        if (defaultHeaders[encryptHeader]) {
          const keyStr = res.header[encryptHeader];
          // 解密数据
          if (keyStr) {
            const base64Str = decrypt(keyStr);
            const aesKey = decryptBase64(base64Str.toString());
            const decryptData = decryptWithAes(responseData as string, aesKey);
            responseData = JSON.parse(decryptData);
          }
        }
        // 处理响应状态码
        if (statusCode === 200) {
          const code = (responseData as any).code || 200; // 默认成功状态
          const msg = (responseData as any).msg || '操作成功';
          // 登录过期处理
          if (code === 401) {
            if (!isRelogin.show) {
              isRelogin.show = true;
              wx.showModal({
                title: '系统提示',
                content: '登录状态已过期,您可以继续留在该页面,或者重新登录',
                confirmText: '重新登录',
                cancelText: '取消',
                success: (modalRes) => {
                  isRelogin.show = false;
                  if (modalRes.confirm) {
                    removeToken();
                    wx.reLaunch({
                      url: '/pages/login/index'
                    });
                  }
                }
              });
            }
            return reject('无效的会话,或者会话已过期,请重新登录。');
          } else if (code !== 200) {
            wx.showToast({
              title: msg,
              icon: 'none',
              duration: 2000
            });
            return reject(new Error(msg));
          } else {
            return resolve(responseData as T);
          }
        } else {
          wx.showToast({
            title: `接口异常:${statusCode}`,
            icon: 'none',
            duration: 2000
          });
          return reject(new Error(`接口异常:${statusCode}`));
        }
      },
      fail: (err) => {
        wx.showToast({
          title: '请求失败,请检查网络',
          icon: 'none',
          duration: 2000
        });
        return reject(err);
      }
    });
  });
};

export default request;

我们的api加解密就可以了!!!大功告成qwq

之后的话就是样式的编写了,登录啊和后端接口都是一样的,我并未做什么修改,大家需要源码可以联系 VX:chenbai0511.页面展示如下

大家需要可以联系作者:chenbai0511

相关推荐
金融数据出海几秒前
使用Spring Boot对接印度股票数据源:实战指南
后端
ONE_Gua2 分钟前
魔改chromium——源码拉取及编译
前端·后端·爬虫
计算机程序设计开发10 分钟前
相机租赁网站基于Spring Boot SSM
spring boot·后端·数码相机·毕设·计算机毕设
__淡墨青衫__11 分钟前
Django之旅:第六节--mysql数据库操作增删改查(二)
后端·python·django
Codelinghu26 分钟前
做后端的我在公司造了一个前端轮子,领导:嘿!你他娘的真是个天才。
前端
小old弟31 分钟前
vue3模板中ref的实现原理
前端·vue.js
京东云开发者33 分钟前
业务复杂度治理方法论--十年系统设计经验总结
后端
陈珙_SkyChen34 分钟前
后端思维之高并发方案
后端
招风的黑耳35 分钟前
ElementUI元件库——提升Axure原型设计效率与质量
前端·elementui·axure
Captaincc38 分钟前
用MCP 让Claude控制ChatGPT 4o,自动生成吉卜力风格的分镜
前端·claude·mcp