【DDD-实践】社保公积金,工资计算功能的实现

目录

背景

人事云平台,需要实现计算工资,社保公积金的逻辑,同时给出 个人的工资条 和 企业需要支付的官费账单。

  • 各地的社保计算规则差距太大,进位规则,保留位数不一样。
  • 一个城市还有很多种社保方案,每个方案的每个单项可能有单独的基数上下限,比例,固定值。

实现

IDEA 生成的 时序图(有删减)

最小的计算单元,包含了地区无关的最少的计算信息:

InsuranceConfigDetail {
    @Id
    private Integer id;

    /**
     * 参保规则ID
     */
    @Column(name = "config_id")
    private Long configId;

    /**
     * 类型#1=养老,2=医疗,3=失业,4=共赏,5=生育,6=残保金,7=公积金,8=大病互助
     */
    @Column(name = "type_code")
    private Integer typeCode;

    /**
     * 付款方#1=公司,2=个人
     */
    @Column(name = "payer_code")
    private Integer payerCode;

    /**
     * 计算类型#1=rate(比例),2=value(固定值)
     */
    @Column(name = "rule_code")
    private Integer ruleCode;

    /**
     * 类别#1=社保,2=公积金
     */
    @Column(name = "cate_code")
    private Integer cateCode;

    @Column(name = "min_base")
    private BigDecimal minBase;

    @Column(name = "max_base")
    private BigDecimal maxBase;
    /**
     * 比例
     */
    private BigDecimal rate;
    /**
     * 固定值
     */
    private BigDecimal value;
}

调用链:
MSalaryService.cal(yearMonth, false, shouldDays, companyId); // 计算工资
|-SalaryDomain.calInsurance(InsuranceConfigDomain config) // 计算社保公积金
|-InsuranceCalculator.cal(config, SalaryDomain); // 社保公积金计算器
|-SalaryDetail.cal(base, scale, mode); // 实际计算位置

// com.zhijia.work.service.manage.MSalaryService#cal
List<SalaryDomain> domains = workers
.stream()
.filter(
	//过滤不需要计算的人
	// - 没有合同信息
	// - 员工已离职,且离职日期早于 社保公积金最后缴纳日期
)
.map(
	// 1. 冗余 worker_job_info 相关信息
	SalaryDomain record = SalaryDomain.create(worker);
	// 计算工资
)
.peek(
	// 计算社保公积金
	InsuranceConfigDomain config = idAndConfig.get(record.getCompanyInsuranceConfigId());
	try {
		record.calInsurance(config);
	} catch (NativeException e) {
		log.error(e.getMessage());
		throw new RollBackException(e.getCode(), e.getMessage());
	}
)
.peek(calOnCheck)   //加班 缺勤工资计算
.peek(SalaryDomain::calRest)   //计算 其他部分
.peek(record -> record.fillDate(yearMonth, today))  //时间计算
.peek(record -> {   //设置身份证信息
	/// ...
})
.collect(Collectors.toList());
    public static WorkerInsuranceDto cal(InsuranceConfigDomain config, SalaryDomain info) throws NativeException {
        List<InsuranceConfigDetailDomain> configDetails = config.getDetails();
        List<InsuranceConfigDetailDomain> salaryDetails = configDetails.stream().
                map(configDetail -> {
                    // config => 工资记录
                    InsuranceConfigDetailDomain salaryDetail = new InsuranceConfigDetailDomain();
                    BeanUtils.copyProperties(configDetail, salaryDetail);
                    return salaryDetail;
                })
                .peek(salaryDetail -> {
                    BigDecimal base = info.getBaseByType(salaryDetail.getType());
                    InsuranceRule rule;
										// 按照 configId 和 payerCode 获取具体的计算规则。
                    if (salaryDetail.isCateInsurance()) {
											// 是社保
                        rule = InsuranceRule.matchInsuranceRule(salaryDetail.getConfigId(), salaryDetail.getPayerCode());
                    } else {
											// she 公积金
                        rule = InsuranceRule.matchFundRule(salaryDetail.getConfigId(), salaryDetail.getPayerCode());
                    }
                    Integer scale = rule.getScale();
                    RoundingMode mode = rule.getMode();
										// 调用 实际计算方法
                    BigDecimal result = salaryDetail.cal(base, scale, mode);
                    salaryDetail.setResult(result);
                }).collect(Collectors.toList());

        WorkerInsuranceDto dto = details2dto(salaryDetails);
        return dto;
    }
public class InsuranceConfigDetailDomain extends InsuranceConfigDetail {
	    /**
     * @param employeeBase 单项基数
     * @param scale        进位 保留位数
     * @param mode         进位规则
     * @return
     */
    public BigDecimal cal(BigDecimal employeeBase, Integer scale, RoundingMode mode) {
        // 该员工不缴纳社保(公积金)? =  社保(公积金)基数 == 0
        boolean notOpen = BigDecimal.ZERO.compareTo(employeeBase) == 0;
        if (notOpen) {
            return BigDecimal.ZERO;
        }

        BigDecimal base = actualBase(employeeBase);
        if (isRuleRate()) {
					// 是 比例
            BigDecimal result = base.multiply(getRate())
                    .divide(ONE_HUNDRED, scale, mode);
            return result;
        } else {
					// 是 值
            return getValue();
        }
    }
    /**
     * 最终生效的基数
     *
     * @param employeeBase
     * @return
     */
    private BigDecimal actualBase(BigDecimal employeeBase) {
        BigDecimal base = employeeBase
                .min(getMaxBase())
                .max(getMinBase());
        return base;
    }
		// 其他工具类
		groupByType
		groupByPayer
		isRuleRate
		isCateInsurance
}

InsuranceRule 保存了 各个地区社保计算规则:
一个元组,包括:

  • configId:按照城市,地区,档位分类。
  • 付款方:公司,个人。
  • 进位规则:四舍五入,向上取整。
  • 保留位数:1,2,3,4。
    把所有相关的配置都封闭在这个类中,将改动限制到最小。

在 static block 中初始化,注册规则。
提供按照 规则 地区 ID 和 公司/个人 查询具体规则。

public class InsuranceRule { 
    private Long configId;
		// 付款方
    private Integer payerCode;
		// 保留位数
    private Integer scale;
		// 进位规则
    private RoundingMode mode;
    static {
        /**
         * (10, '苏州', '市区'),
         (11, '苏州', '工业园区甲类'),
         (12, '苏州', '工业园区乙类'),
         (20, '北京', '市区'),
         (21, '北京', '农村'),
         (30, '深圳', '一档'),
         (31, '深圳', '二档'),
         (32, '深圳', '三档'),
         (40, '镇江', '市区'),
         (50, '上海', '默认'),
         (60, '南京', '默认'),
         (70, '合肥', '默认'),
         (80, '成都', '默认'),
         */
        //todo use Enum instead
        /**
         * 社保
         * 默认 四舍五入到两位
         */
        InsuranceRule baseRule = new InsuranceRule(0L, 0, 2, RoundingMode.HALF_UP);
        insuranceRules.add(baseRule);
        // 合肥 公司 四舍五入 3位
        insuranceRules.add(new InsuranceRule(70L, 1, 3, RoundingMode.HALF_UP));
        // 上海 公司 四舍五入 4位
        insuranceRules.add(new InsuranceRule(50L, 1, 4, RoundingMode.HALF_UP));
        // 上海 个人 向上取整 1位
        insuranceRules.add(new InsuranceRule(50L, 2, 1, RoundingMode.UP));

        /**
         * 公积金
         * 默认 四舍五入到整数
         */
        InsuranceRule baseRule1 = new InsuranceRule(0L, 0, 0, RoundingMode.UP);
        // 苏州  园区  个人和公司 四舍五入 2位
        fundRules.add(baseRule1);
        fundRules.add(new InsuranceRule(11L, 1, 2, RoundingMode.HALF_UP));
        fundRules.add(new InsuranceRule(11L, 2, 2, RoundingMode.HALF_UP));
        fundRules.add(new InsuranceRule(12L, 1, 2, RoundingMode.HALF_UP));
        fundRules.add(new InsuranceRule(12L, 2, 2, RoundingMode.HALF_UP));
    }
    public static InsuranceRule matchInsuranceRule(Long configId, Integer payerCode) {
        Optional<InsuranceRule> first = insuranceRules.stream()
                .filter(rule -> configId.equals(rule.configId) && payerCode.equals(rule.payerCode))
                .findFirst();
        return first.orElse(insuranceRules.get(0));
    }

    public static InsuranceRule matchFundRule(Long configId, Integer payerCode) {
        Optional<InsuranceRule> first = fundRules.stream()
                .filter(rule -> configId.equals(rule.configId) && payerCode.equals(rule.payerCode))
                .findFirst();
        return first.orElse(fundRules.get(0));
    }
}

计算工资其他部分的核心代码:
也就是 上面 calRest 中的实现。
通过一个顺序严格的链式调用,计算工资的各个部分,
规则:

  • 应发工资=基本工资+其他工资+补贴+加班工资-缺勤扣款
  • 应税工资=应发工资-社保公积金_个人部分
  • 实际工资=应税工资-个人所得税
  • 官费=应发工资+社保公积金_公司部分

同时,计算五险一金的调用也是在 SalaryDomain 中。

  • 计算社保公积金
  • 填充各个单项的值
  • 计算社保公积金_个人部分 总和
  • 计算社保公积金_公司部分 总和
public class SalaryDomain extends WorkerSalaryRecord
        implements DomainMixin<WorkerSalaryRecord, SalaryDomain>,
        InsuranceMixin,
        LoggerMixin {
		/**
     * 计算五险一金
     *
     * @return
     */
    public SalaryDomain calInsurance(InsuranceConfigDomain config) throws NativeException {
        logger().debug("calInsurance");

        if (config == null) {
            throw new NativeException("[salary.calInsurance]社保公积金配置为空");
        }
        WorkerInsuranceDto insuranceDto = InsuranceCalculator.cal(config, this);
        // 2. 五险一金明细
        fillInsuranceDetails(insuranceDto);
        return this;
    }
    /**
     * 计算
     * 1. 应发工资
     * 2. 应税工资
     * 3. 个人所得税
     * 4. 实发工资
     * 5. 官费
     * <p>
     * required:
     * getBasicWage
     * getOtherWage
     * getTrafficAllow
     * getMealAllow
     * getHighTempAllow
     * getOtherAllow
     * getOvertimeWage
     * getAbsenceWage
     * getPersonTotal
     * getCompTotal
     *
     * @return
     */
    public SalaryDomain calRest() {
        return calMustWage()  //应发工资
                .calMustTaxWage()//应税工资
                .calTax()   //个人所得税
                .calActualWage()   //实际工资
                .calFeeTotal()    //计算官费=应发工资+社保公积金_公司部分
                ;
    }
				}