项目结构与计算难点
这个示例由两个 Python 文件组成:salary_funcs.py 负责五险一金和个税计算,calc_salary_ui.py 负责 Flask 接口与前端页面。工资计算并不只是“税前收入乘比例”,核心是累计预扣法:截至当月的累计收入先减去累计五险一金、累计专项附加扣除和每月5000元乘以月份数的累计免征额,再套年度税率表算出累计应缴个税,最后减去已预缴税额,得到当月应补税。
五险一金个人缴纳函数
个人缴费常见比例为养老8%、医疗2%、失业0.5%(部分城市0.2%),公积金在5%到12%之间。函数 calc_social_insurance 接收税前月薪、社保基数、公积金比例;若 base_salary 为0,则退化为使用当月工资作为基数。社保基数在真实业务中常取上年度月平均工资,所以要外部传入而不能写死。
- def calc_social_insurance(salary, base_salary, housing_fund_rate=0.12):
- if base_salary == 0:
- base_salary = salary
- pension = base_salary * 0.08
- medical = base_salary * 0.02
- unemployment = base_salary * 0.005
- housing_fund = base_salary * housing_fund_rate
- total = pension + medical + unemployment + housing_fund
- return {
- 'pension': round(pension, 2),
- 'medical': round(medical, 2),
- 'unemployment': round(unemployment, 2),
- 'housing_fund': round(housing_fund, 2),
- 'total': round(total, 2)
- }
复制代码
这里 housing_fund_rate 默认0.12,可按公司实际改为0.05、0.07或0.10。返回字典包含养老、医疗、失业、公积金和合计,便于前端展示。
累计预扣法个税函数
个税函数最容易出错的地方是免征额。原文先给出一个按月免征额参数的设计,随后指出:如果每次调用都传5000,累计到第二个月仍然只减5000,结果就错了。正确做法是传入 cumulative_months,在函数内部计算 5000 * cumulative_months,或者由调用方传入 5000 乘当前月份数。
- def calc_monthly_tax(cumulative_income, cumulative_insurance, cumulative_deduction,
- pre_paid_tax, cumulative_months):
- tax_income = cumulative_income - cumulative_insurance - cumulative_deduction - 5000 * cumulative_months
- tax_income = max(tax_income, 0)
- brackets = [
- (36000, 0.03, 0),
- (144000, 0.10, 2520),
- (300000, 0.20, 16920),
- (420000, 0.25, 31920),
- (660000, 0.30, 52920),
- (960000, 0.35, 85920),
- (float('inf'), 0.45, 181920)
- ]
- payable_tax = 0
- for limit, rate, quick_deduction in brackets:
- if tax_income <= limit:
- payable_tax = tax_income * rate - quick_deduction
- break
- monthly_tax = max(payable_tax - pre_paid_tax, 0)
- return round(monthly_tax, 2)
复制代码
税率表按年度累计所得分七档:36000以内3%、144000以内10%、300000以内20%、420000以内25%、660000以内30%、960000以内35%,超过部分45%,并分别对应速算扣除数0、2520、16920、31920、52920、85920、181920。cumulative_deduction 表示截至当月累计的专项附加扣除和其他扣除,不是单月值;若中途变更,必须传入累计后的值。
用数据类收拢收入参数
收入不止基本工资,还可能包括绩效奖金、加班费、津贴、补发工资等。为了避免函数参数膨胀,可以用 dataclass 定义 SalaryInput,把字段集中在 API 层映射。
- from dataclasses import dataclass
- @dataclass
- class SalaryInput:
- basic: float
- bonus: float = 0
- overtime: float = 0
- allowance: float = 0
- social_base: float = 0
- housing_rate: float = 0.12
- deductions: float = 0
复制代码
基本工资 basic 必填,其余字段都有默认值。这样单元测试只需写关键字段,后续增加“年终奖单独计税”时,也可以在另一个函数中处理 bonus,而不影响综合所得逻辑。
Flask 接口设计
后端把计算函数暴露成 POST /api/calculate,请求体用 JSON,字段名与 SalaryInput 一致。pre_data 保存截至上月的累计月份和已预缴税。示例中为了演示没有接数据库,所以每次调用需要前端把已缴税额传回来;生产环境应改为数据库或会话存储。
- from flask import Flask, request, jsonify, render_template_string
- from salary_funcs import calc_social_insurance, calc_monthly_tax
- app = Flask(__name__)
- def compute_month(salary_input, pre_data):
- insurance = calc_social_insurance(
- salary_input.basic + salary_input.bonus,
- salary_input.social_base,
- salary_input.housing_rate
- )
- taxable_income = (salary_input.basic + salary_input.bonus + salary_input.overtime
- + salary_input.allowance) - insurance['total']
- monthly_tax = calc_monthly_tax(
- cumulative_income=taxable_income * pre_data['months'],
- cumulative_insurance=insurance['total'] * pre_data['months'],
- cumulative_deduction=salary_input.deductions * pre_data['months'],
- pre_paid_tax=pre_data['paid_tax'],
- cumulative_months=pre_data['months']
- )
- net_salary = taxable_income - monthly_tax
- return {
- 'insurance': insurance['total'],
- 'tax': monthly_tax,
- 'net_salary': round(net_salary, 2)
- }
- @app.route('/api/calculate', methods=['POST'])
- def calculate():
- data = request.get_json()
- salary_input = SalaryInput(
- basic=float(data.get('basic', 0)),
- bonus=float(data.get('bonus', 0)),
- overtime=float(data.get('overtime', 0)),
- allowance=float(data.get('allowance', 0)),
- social_base=float(data.get('social_base', 0)),
- housing_rate=float(data.get('housing_rate', 0.12)),
- deductions=float(data.get('deductions', 0))
- )
- pre_data = {
- 'months': int(data.get('months', 1)),
- 'paid_tax': float(data.get('paid_tax', 0))
- }
- result = compute_month(salary_input, pre_data)
- return jsonify(result)
复制代码
注意示例中的 compute_month 先算 insurance,再把 taxable_income 传入 calc_monthly_tax,同时又把 insurance 作为 cumulative_insurance 传入。如果 taxable_income 已经扣过五险一金,就会重复扣除;更清晰的划分是让 cumulative_income 传累计总收入,累计五险一金单独传,或者把已扣保险的金额与累计保险参数二选一。这个细节在二次开发时要特别检查。
前端页面与 fetch 交互
calc_salary_ui.py 可以用 render_template_string 直接在 Python 字符串里维护 HTML,不需要单独创建 templates 目录。表单字段建议分三组:收入信息(基本工资、绩效奖金、加班费、津贴)、社保公积金(社保基数、公积金比例)、累计信息(当前月份、当年已缴个税、专项附加扣除)。公积金比例用下拉框限制为5%、7%、10%、12%,减少无效输入。
提交时不要整页刷新,用 fetch 发 POST,然后把 JSON 渲染成表格。renderResult 中可以把五险一金个人缴纳、当月个税、到手工资、累计个税逐行放入 table,比直接拼 innerHTML 更清晰。
- form.addEventListener('submit', async (e) => {
- e.preventDefault();
- const data = Object.fromEntries(new FormData(form));
- try {
- const res = await fetch('/api/calculate', {
- method: 'POST',
- body: JSON.stringify(data),
- headers: {'Content-Type': 'application/json'}
- });
- if (!res.ok) {
- const error = await res.json();
- throw new Error(error.message || '接口错误');
- }
- const result = await res.json();
- renderResult(result);
- } catch (err) {
- document.getElementById('errorBox').textContent = '计算失败: ' + err.message;
- }
- });
复制代码
参数校验与常见排错
前端能拦空值和类型错误,但后端必须做最终检查。月份必须在1到12之间,公积金比例必须在0.05到0.12之间,请求体必须是 JSON。不符合时返回 HTTP 400 和中文错误信息,前端根据 res.ok 捕获并显示。
- months = int(data.get('months', 1))
- if months < 1 or months > 12:
- return jsonify({'message': '月份必须在1到12之间'}), 400
- housing_rate = float(data.get('housing_rate', 0.12))
- if housing_rate < 0.05 or housing_rate > 0.12:
- return jsonify({'message': '公积金比例需在0.05到0.12之间'}), 400
复制代码
常见问题包括:FormData 取出的值都是字符串,后端要用 float 或 int 转换;已缴税字段首次使用填0,连续计算时要把上月输出中的累计个税回填;专项附加扣除如果每月不同,不能简单用 deductions * months,而要传累计值;社保基数上下限、失业保险比例、公积金比例各地不同,需要在 calc_social_insurance 外先确定缴费基数。
适用场景与局限
这个工资计算器适合学习累计预扣法、做 HR/财务薪酬测算原型,或嵌入已有管理系统做二次开发。它没有数据库,状态需要手动传递;税率表、五险一金比例和基数上下限属于示例参数,真实使用要按当地政策和公司口径替换。Flask 在这里承担轻量 API 和页面渲染,不引入额外前端框架,也方便继续改造成前后端分离接口。 |