AIGC标识 从零写个税模拟器:收入记录建模与只读查询源码

做个税模拟器的教学页面时,一个月份可能对应多条收入记录。本文结合我维护的 Mou652 项目,给出一段独立源码,演示怎样保留记录身份、生成查询快照,并让列表和合计采用同一口径。

从记录模型开始

这是一篇界面教学程序的实现练习。示例在内存里工作,使用固定的虚构金额与年度,不连接税务账户。预扣税额是样本中预置的展示值,程序只做筛选和加总,不根据收入推算税额。

先给每条记录一个稳定的 id。月份表示时间范围,id 表示某一笔记录。两条记录同属一月,甚至金额相同,也不能因为月份相同就互相覆盖。页面列表、单笔详情和汇总都应围绕同一组记录组织。

本例的数据约定是:记录为扁平对象,字段只含字符串和数字;id 是上游已校验的唯一字符串;年度和月份是整数;收入与预置税额均以整数“分”保存。示例函数会检查查询月份和选中记录的金额,不承担完整的数据导入校验。

先写出期望结果

准备五条记录:2025 年一月两笔工资薪金、五月一笔工资薪金、一月一笔劳务示例,以及 2024 年的一笔工资薪金。查询条件是 2025 年、一月和五月、工资薪金。

记录 年度与月份 类型 收入(分) 预置税额(分)
a 2025-01 salary 600000 10000
b 2025-01 salary 650000 12000
c 2025-05 salary 700000 14000
d 2025-01 labor 90000 0
e 2024-01 salary 800000 0

正确结果是三笔记录、覆盖两个月,收入合计 1950000 分、预置税额合计 36000 分。换成元分别为 19500.00 和 360.00。这里不评价示例税额是否符合任何实际税务规则。

核心代码:生成一份查询结果

把下面的完整代码保存为 income-view.mjs,在已有 Node.js 环境执行 node income-view.mjs 即可看到输出。不需要安装第三方依赖。它是为本文独立编写的教学实现,代码按 MIT 许可提供,并非手机 App 私有工程的摘录。

// SPDX-License-Identifier: MIT
// Copyright (c) 2026 Mou652
export function buildIncomeView(records, { year, months, incomeType }) {
  if (!Number.isInteger(year) || !Array.isArray(months) ||
      !months.every(m => Number.isInteger(m) && m >= 1 && m <= 12)) {
    throw new TypeError('年度或月份不符合约定');
  }
  const allowedMonths = new Set(months);

  // 生成当前条件下的记录副本
  const rows = records
    .filter(r => r.year === year && r.incomeType === incomeType &&
      allowedMonths.has(r.month))
    .map(r => {
      for (const amount of [r.incomeFen, r.withheldFen]) {
        if (!Number.isSafeInteger(amount) || amount < 0) {
          throw new TypeError('示例金额必须为非负安全整数分');
        }
      }
      return Object.freeze({ ...r });
    })
    .sort((a, b) => b.month - a.month || a.id.localeCompare(b.id));

  // 从同一份明细计算收入与预置税额
  const incomeFen = rows.reduce((n, r) => n + r.incomeFen, 0);
  const withheldFen = rows.reduce((n, r) => n + r.withheldFen, 0);
  if (![incomeFen, withheldFen].every(Number.isSafeInteger)) {
    throw new RangeError('合计超出安全整数范围');
  }
  return Object.freeze({
    year, incomeType, rows: Object.freeze(rows),
    rowCount: rows.length,
    monthCount: new Set(rows.map(r => r.month)).size,
    incomeFen, withheldFen,
    notice: '虚构示例,仅用于界面教学'
  });
}

const records = [
  { id: 'a', year: 2025, month: 1, incomeType: 'salary', incomeFen: 600000, withheldFen: 10000 },
  { id: 'b', year: 2025, month: 1, incomeType: 'salary', incomeFen: 650000, withheldFen: 12000 },
  { id: 'c', year: 2025, month: 5, incomeType: 'salary', incomeFen: 700000, withheldFen: 14000 },
  { id: 'd', year: 2025, month: 1, incomeType: 'labor', incomeFen: 90000, withheldFen: 0 },
  { id: 'e', year: 2024, month: 1, incomeType: 'salary', incomeFen: 800000, withheldFen: 0 }
];
const view = buildIncomeView(records, {
  year: 2025, months: [1, 5], incomeType: 'salary'
});
console.log({
  rowCount: view.rowCount, monthCount: view.monthCount,
  incomeFen: view.incomeFen, withheldFen: view.withheldFen
});

终端输出应为:

{ rowCount: 3, monthCount: 2, incomeFen: 1950000, withheldFen: 36000 }

这段源码做了哪些取舍

筛选只在一个地方完成。先得到 rows,再从 rows 计算收入、预置税额和覆盖月份。接到界面时,列表渲染 rows,顶部汇总读取 incomeFen 与 withheldFen,详情按照 id 从同一份 rows 查找,三个区域便有了明确的数据来源。

months 通过集合表达可选月份。一月在查询条件中出现两次,不会把记录复制两遍;传入空数组则返回空结果。本例不把空数组偷偷解释成“全年”,调用者需要全年时应明确传入一至十二月。

记录笔数和覆盖月份数分开计算。一月有两条记录时,rowCount 增加两笔,monthCount 只计一个月。不能用月份作为唯一键保存记录,也不能用列表长度直接代表有收入的月份数量。

结果按月份倒序排列,同月按 id 排列。这里的 id 排序只是让演示结果稳定,不代表同月的业务先后顺序;需要展示申报时间时,应增加相应字段和明确规则。

为什么要复制并冻结记录

只复制数组,里面的对象仍可能和编辑区共享。这里逐条复制记录,再冻结记录、数组及结果对象,因此在本例的扁平数据约定下,修改原始记录不会改变已生成的查询快照。界面要更新时,重新生成下一份结果。

冻结是内存中的变化约束,不等于写入磁盘、事务提交或安全防护。如果以后加入嵌套对象,浅复制与浅冻结就不足以隔开内部变化,需要重新定义数据结构或复制策略。手机端的保存、恢复和异步请求也需要各自实现。

金额在安全整数范围内加总,超过范围直接报错。展示时再把分格式化成元;不要一边累加格式化字符串,一边依赖页面文本反向计算。这一练习只支持非负示例金额,退款或更正记录需要单独定义规则。

怎样验证后再接到界面

可以围绕同一组输入检查这些情况:

  • 同月两笔记录都保留,详情仍能按不同 id 找到
  • 把月份改成 [1],结果为两笔、一个月、收入 1250000 分
  • 选择没有样本的年度或传入空月份,列表为空、两个合计都为零
  • 重复选择同一个月份,记录不重复;改变原始记录,旧快照不跟着改变
  • 选中记录含负数、小数分或超出安全整数的金额,得到明确错误

接入界面时,可以先展示“条件、明细、合计、虚构示例说明”四个区域,再补加载和错误提示。异步查询需要避免旧请求覆盖新条件;本文没有实现网络、持久化或移动端页面,独立示例的运行结果也不代表整套 App 的设备验证。

项目资料与交流

本文由 Mou652 个税模拟器维护者整理。该项目提供安卓 Android 与 iPhone/iOS 的非官方界面教学演示,可配置虚构资料,讲解年度查询、收入明细和截图录屏。

GitHub 项目与教程 · 官网使用说明

公开仓库目前以文档、界面展示和使用教程为主,不包含可构建的完整 App 主体源码。本文已给出独立练习的全部代码,可以直接运行;代码问题可在评论区或项目 Issues 中交流。版本与教学使用咨询 QQ:2430239942。

创作说明:本文含人工智能辅助创作内容,示例数据均为虚构,仅用于技术学习与界面教学。

posted @ 2026-11-09 11:52  MouCoder  阅读(3)  评论(0)    收藏  举报