前情回顾与本节目标

在上一节中,我们完成了薪酬与提成引擎功能,实现了提成自动计算、薪资结构配置和自动算薪。

本节我们将继续完善OA管理模块,实现工资核算看板功能。这是企业内部HR和财务人员最核心的工作台,需要支持月度工资核算、批量发放、工资条查看、社保个税申报以及异常处理等完整流程。

本节核心目标:

  • 数据模型设计:创建工资记录表、发放批次表、社保配置表、异议反馈表
  • 工资核算看板:月度工资总额、发放进度、异常项数等核心指标
  • 工资核算表:员工工资明细列表,支持批量核算、批量发放、导出工资条
  • 发放记录:按批次查看历史发放记录
  • 我的工资条:员工查看个人工资明细,支持异议反馈
  • 社保个税:参保规则配置和本月申报明细
  • 一键核算:自动核算本月工资,预览结果并确认

在这里插入图片描述


第一步:数据模型准备

1.1 工资记录表(MBA_PayrollRecords)

用于记录每位员工每月的工资明细:

字段名称 字段标识 字段类型 说明
记录ID _id 文本 主键,系统自动生成
关联员工 rel_employee_id 关联关系 关联 MBA_Users 表
工号 employee_no 文本 员工工号
部门 department 文本 所属部门
职位 post 文本 职位名称
核算月份 calc_month 文本 如:2026-02
基本工资 base_salary 数字 基本工资金额
绩效奖金 performance_bonus 数字 绩效奖金金额
业务提成 commission 数字 销售提成金额
补贴加班 allowance 数字 补贴和加班费
考勤扣款 deduction 数字 考勤扣款金额
社保代扣 social_security 数字 社保个人缴纳部分
公积金代扣 housing_fund 数字 公积金个人缴纳部分
个人所得税 tax 数字 个人所得税
应发工资 gross_pay 数字 应发工资合计
实发工资 net_pay 数字 实发工资合计
核算状态 status 枚举 1-待核算、2-已核算、3-已发放、4-异常
异常原因 abnormal_reason 文本 异常原因说明
发放日期 payment_date 日期 实际发放日期
创建时间 created_at 日期时间 自动生成
更新时间 updated_at 日期时间 自动更新

1.2 发放批次表(MBA_PayrollBatches)

用于记录每次工资发放的批次信息:

字段名称 字段标识 字段类型 说明
批次ID _id 文本 主键,系统自动生成
批次编号 batch_no 文本 如:BATCH001
发放月份 pay_month 文本 如:2026-02
发放日期 payment_date 日期 实际发放日期
发放人数 employee_count 数字 本次发放人数
发放总金额 total_amount 数字 本次发放总金额
发放方式 pay_method 枚举 1-银行代发、2-微信支付、3-现金发放
发放状态 status 枚举 1-已发放、2-部分发放、3-发放失败
创建时间 created_at 日期时间 自动生成
更新时间 updated_at 日期时间 自动更新

1.3 社保配置表(MBA_TaxInsuranceConfig)

用于配置社保和公积金的计算参数:

字段名称 字段标识 字段类型 说明
配置ID _id 文本 主键,系统自动生成
参保城市 city 文本 参保城市名称
社保基数下限 social_base_min 数字 社保最低缴费基数
公积金基数下限 fund_base_min 数字 公积金最低缴费基数
社保个人比例 social_rate 数字 社保个人缴纳比例(%)
公积金个人比例 fund_rate 数字 公积金个人缴纳比例(%)
生效日期 effective_date 日期 配置生效日期
创建时间 created_at 日期时间 自动生成
更新时间 updated_at 日期时间 自动更新

1.4 异议反馈表(MBA_PayrollDisputes)

用于记录员工对工资的异议反馈:

字段名称 字段标识 字段类型 说明
反馈ID _id 文本 主键,系统自动生成
关联工资记录 rel_payroll_id 关联关系 关联 MBA_PayrollRecords 表
反馈人 rel_employee_id 关联关系 关联 MBA_Users 表
反馈内容 content 多行文本 异议反馈内容
处理状态 status 枚举 1-待处理、2-处理中、3-已处理
处理结果 result 多行文本 财务复核结果
处理人 rel_handler_id 关联关系 关联 MBA_Users 表(财务人员)
处理时间 handled_at 日期时间 处理时间
创建时间 created_at 日期时间 自动生成
更新时间 updated_at 日期时间 自动更新

第二步:工资核算看板页面搭建

2.1 创建工资核算页面

点击创建页面图标,选择表格与表单模板,数据模型选择工资记录表,选择财务布局
在这里插入图片描述

切换到布局管理,选择财务布局,添加平级菜单,添加"工资核算"菜单
在这里插入图片描述

2.2 页面顶部区域

切换回页面设计,在财务布局的内容插槽下添加布局组件,修改标题为"工资核算看板"
在这里插入图片描述


第二步:统计卡片区域

2.1 统计卡片配置

在顶部区域下方添加网格布局
在这里插入图片描述
选中行组件,设置列的数量为5
在这里插入图片描述

在容器内添加5个卡片组件,分别展示核心指标:

卡片 指标 数据来源
本月工资总额 ¥254,000 汇总当月工资记录应发工资
已发放金额 ¥0 汇总当月已发放状态的实发工资
待发放金额 ¥254,000 汇总当月非已发放状态的实发工资
异常项数 1 统计当月异常状态的记录数
社保个税代扣 ¥12,000 汇总当月社保+公积金+个税

在这里插入图片描述

3.2 加载仪表盘数据

创建自定义变量,用来存储统计卡片的数据
在这里插入图片描述

创建自定义方法 loadDashboardData

export default async function loadDashboardData({ event, data }) {
  try {
    // 获取当前月份的第一天毫秒值
    const today = new Date();
    const currentMonth = new Date(today.getFullYear(), today.getMonth(), 1).getTime();

    const payrollRes = await $w.cloud.callDataSource({
      dataSourceName: 'MBA_PayrollRecords',
      methodName: 'wedaGetRecordsV2',
      params: {
        filter: { where: { calc_month: { $eq: currentMonth } } },
        select: { $master: true }
      }
    });

    const records = payrollRes.records || [];

    const totalGross = records.reduce((sum, r) => sum + (r.gross_pay || 0), 0);
    const paidAmount = records
      .filter(r => r.status === '3')
      .reduce((sum, r) => sum + (r.net_pay || 0), 0);
    const pendingAmount = records
      .filter(r => r.status !== '3')
      .reduce((sum, r) => sum + (r.net_pay || 0), 0);
    const abnormalCount = records.filter(r => r.status === '4').length;
    const taxTotal = records.reduce((sum, r) =>
      sum + (r.social_security || 0) + (r.housing_fund || 0) + (r.tax || 0), 0
    );

    const payProgress = totalGross > 0 ? Math.round((paidAmount / (paidAmount + pendingAmount)) * 100) : 0;

    $w.page.dataset.state.dashboardData = {
      totalGross,
      paidAmount,
      pendingAmount,
      abnormalCount,
      taxTotal,
      payProgress
    };

  } catch (error) {
    console.error('加载仪表盘数据失败:', error);
  }
}

在页面加载时调用 loadDashboardData 方法。
在这里插入图片描述
给卡片的统计结果文本绑定对应的字段
在这里插入图片描述


第三步:标签页配置

3.1 标签页组件

在统计卡片下方添加标签页组件,配置五个标签页:

标签页值 名称 内容组件
payroll-table 工资核算表 工资明细表格
history 发放记录 发放批次列表
my-payslip 我的工资条 个人工资条
tax-insurance 社保&个税 参保配置和申报明细
abnormal 异常处理 异常项处理队列

在这里插入图片描述


第四步:工资核算表

4.1 批量操作工具栏

在全局按钮里添加批量操作按钮:

按钮 功能
批量核算 对选中记录执行核算
批量发放 对选中记录执行发放
导出工资条 导出选中记录的工资条

在这里插入图片描述

4.2 工资明细表格

添加数据表格组件,数据模型选择工资记录表(MBA_PayrollRecords)

配置表格列:

列名 绑定字段 说明
复选框 - 多选操作
员工姓名 rel_employee_id.name 关联查询员工姓名
部门/职位 department / post 部门和职位
底薪 base_salary 基本工资
绩效奖金 performance_bonus 绩效奖金
提成金额 commission 业务提成
补贴/扣款 allowance / deduction 补贴和扣款
应发工资 gross_pay 应发合计
实发工资 net_pay 实发合计
状态 status 核算状态
操作 - 操作菜单

4.3 批量核算功能

创建自定义方法 batchCalculate

export default async function batchCalculate({ event, data }) {
  try {
    $w.utils.showLoading({ title: '核算中...' });

    // 获取当前月份
    const today = new Date();
    const currentMonth = new Date(today.getFullYear(), today.getMonth(), 1).getTime();

    // 查询社保配置
    const configRes = await $w.cloud.callDataSource({
      dataSourceName: 'MBA_TaxInsuranceConfig',
      methodName: 'wedaGetRecordsV2',
      params: {
        filter: { where: {} },
        select: { $master: true }
      }
    });

    const config = configRes.records?.[0] || {};
    const socialRate = (config.social_rate || 10.5) / 100;
    const fundRate = (config.fund_rate || 7) / 100;
    const socialBaseMin = config.social_base_min || 7310;
    const fundBaseMin = config.fund_base_min || 2800;

    // 1. 从薪资计算记录表获取当月所有记录
    const salaryRes = await $w.cloud.callDataSource({
      dataSourceName: 'MBA_SalaryRecords',
      methodName: 'wedaGetRecordsV2',
      params: {
        filter: {
          where: {
            calc_month: { $eq: currentMonth }
          }
        },
        select: { $master: true }
      }
    });

    const salaryRecords = salaryRes.records || [];

    if (salaryRecords.length === 0) {
      $w.utils.hideLoading();
      return $w.utils.showToast({ title: '当月无薪资计算记录', icon: 'warning' });
    }

    // 2. 查询当月已有的工资记录(用于判断是更新还是创建)
    const existingRes = await $w.cloud.callDataSource({
      dataSourceName: 'MBA_PayrollRecords',
      methodName: 'wedaGetRecordsV2',
      params: {
        filter: {
          where: {
            calc_month: { $eq: currentMonth }
          }
        },
        select: { $master: true }
      }
    });

    const existingMap = new Map();
    (existingRes.records || []).forEach(r => {
      existingMap.set(r.rel_employee_id?._id || r.rel_employee_id, r);
    });

    // 3. 查询上月工资记录(用于异常检测)
    const prevMonth = new Date(currentMonth);
    prevMonth.setMonth(prevMonth.getMonth() - 1);
    const prevMonthTime = new Date(prevMonth.getFullYear(), prevMonth.getMonth(), 1).getTime();

    const prevRes = await $w.cloud.callDataSource({
      dataSourceName: 'MBA_PayrollRecords',
      methodName: 'wedaGetRecordsV2',
      params: {
        filter: {
          where: {
            calc_month: { $eq: prevMonthTime }
          }
        },
        select: { $master: true }
      }
    });

    const prevMap = new Map();
    (prevRes.records || []).forEach(r => {
      prevMap.set(r.rel_employee_id?._id || r.rel_employee_id, r);
    });

    // 4. 逐条核算
    for (const salaryRecord of salaryRecords) {
      const employeeId = salaryRecord.rel_salesperson_id?._id || salaryRecord.rel_salesperson_id;

      // 从薪资表获取基础数据
      const baseSalary = salaryRecord.base_salary || 0;
      const performanceBonus = salaryRecord.performance_salary || 0;
      const commission = salaryRecord.commission_total || 0;

      // 获取员工信息
      const empRes = await $w.cloud.callDataSource({
        dataSourceName: 'MBA_Employees',
        methodName: 'wedaGetItemV2',
        params: {
          filter: { where: { _id: { $eq: employeeId } } },
          select: { $master: true }
        }
      });

      const employee = empRes || {};

      // 计算社保公积金
      const socialBase = Math.max(baseSalary, socialBaseMin);
      const fundBase = Math.max(baseSalary, fundBaseMin);
      const socialSecurity = Math.round(socialBase * socialRate);
      const housingFund = Math.round(fundBase * fundRate);

      // 计算应发和实发
      const grossPay = baseSalary + performanceBonus + commission;
      const tax = Math.round(grossPay * 0.03);
      const netPay = grossPay - socialSecurity - housingFund - tax;

      // 异常检测:实发工资涨幅超过30%
      let status = '2';
      let abnormalReason = '';

      const prevRecord = prevMap.get(employeeId);
      if (prevRecord && prevRecord.net_pay > 0 && netPay > prevRecord.net_pay * 1.3) {
        status = '4';
        abnormalReason = '实发工资涨幅超过30%';
      }

      // 判断是更新还是创建
      const existingRecord = existingMap.get(employeeId);

      if (existingRecord) {
        // 更新已有记录
        await $w.cloud.callDataSource({
          dataSourceName: 'MBA_PayrollRecords',
          methodName: 'wedaUpdateV2',
          params: {
            filter: { where: { _id: { $eq: existingRecord._id } } },
            data: {
              base_salary: baseSalary,
              performance_bonus: performanceBonus,
              commission: commission,
              social_security: socialSecurity,
              housing_fund: housingFund,
              tax: tax,
              gross_pay: grossPay,
              net_pay: netPay,
              status: status,
              abnormal_reason: abnormalReason,
              updated_at: Date.now()
            }
          }
        });
      } else {
        // 创建新记录
        await $w.cloud.callDataSource({
          dataSourceName: 'MBA_PayrollRecords',
          methodName: 'wedaCreateV2',
          params: {
            data: {
              rel_employee_id: { _id: employeeId },
              employee_no: employee.employee_no || '',
              department: employee.department || '',
              post: employee.position || '',
              calc_month: currentMonth,
              base_salary: baseSalary,
              performance_bonus: performanceBonus,
              commission: commission,
              allowance: 0,
              deduction: 0,
              social_security: socialSecurity,
              housing_fund: housingFund,
              tax: tax,
              gross_pay: grossPay,
              net_pay: netPay,
              status: status,
              abnormal_reason: abnormalReason,
              created_at: Date.now(),
              updated_at: Date.now()
            }
          }
        });
      }
    }

    $w.utils.hideLoading();
    $w.utils.showToast({
      title: `核算完成,共处理 ${salaryRecords.length} 条记录`,
      icon: 'success'
    });
    $w.table2.refresh();

  } catch (error) {
    console.error('批量核算失败:', error);
    $w.utils.hideLoading();
    $w.utils.showToast({ title: '核算失败,请重试', icon: 'error' });
  }
}

给按钮配置点击事件,调用方法
在这里插入图片描述


最终效果

HR或财务人员进入工资核算看板,查看5个核心指标卡片。

通过标签页切换,可以在工资核算表、发放记录、我的工资条、社保个税和异常处理五个模块之间切换。

在工资核算表中,可以勾选多条记录进行批量核算、批量发放或导出工资条。每条记录展示完整的工资构成:底薪、绩效奖金、提成、补贴扣款、应发工资、实发工资和核算状态。

在这里插入图片描述


总结

本节完成了工资核算看板功能的实现:

  1. 数据模型设计:创建工资记录表(MBA_PayrollRecords)、发放批次表(MBA_PayrollBatches)、社保配置表(MBA_TaxInsuranceConfig)、异议反馈表(MBA_PayrollDisputes)
  2. 工资核算看板:月份切换、发放进度、5个核心指标卡片
  3. 工资核算表:员工工资明细列表,支持批量核算、批量发放、导出工资条
  4. 发放记录:按批次查看历史发放记录
  5. 我的工资条:个人工资明细查看,支持异议反馈
  6. 社保个税:参保规则配置和本月申报明细
  7. 一键核算:自动核算本月工资,异常检测,结果预览确认

通过本节的学习,我们完善了财务管理中的薪资核算流程,实现了从工资数据准备到自动核算再到批量发放的完整闭环。下一节我们将继续完善OA模块的其他功能。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐