feat: 工资查询助手首版 v1.0.105

基于 skill-template 脚手架开发,包含:
- payroll_config(费率配置,可编辑)+ payslip_snapshots(解析历史,只读)2 张业务表
- 全国默认 + 北上广 4 套费率(社保/公积金/个税起征点/7 级税率表)
- 5 个业务 Action:calculate-payslip-simple / calculate-payslip-full / explain-term / view-config / edit-config
- 个税累计预扣法计算,支持多月历史累加
- 20 个薪资名词库(社保/公积金/个税/全勤/加班费等)
- 完整市场文档(README/TUTORIAL/DEMO/CHANGELOG)
- 296 测试全部通过
This commit is contained in:
skill-dev
2026-09-07 17:02:05 +08:00
commit 3db196e49d
205 changed files with 29927 additions and 0 deletions

209
scripts/cli/app.py Normal file
View File

@@ -0,0 +1,209 @@
"""CLI:argparse 与分发模板。
只负责参数解析并转交 service;业务内核在 domain / task_service,勿在此按入口复制逻辑。
"""
from __future__ import annotations
import argparse
import sys
from typing import List, Optional
from service.task_service import (
cmd_calculate_full,
cmd_calculate_simple,
cmd_config_path,
cmd_edit_config,
cmd_explain,
cmd_health,
cmd_init_db,
cmd_run,
cmd_version,
cmd_view_config,
_parse_float_or_none,
)
from util.config_bootstrap import bootstrap_skill_config
from util.argparse_zh import ZhArgumentParser
from util.constants import LOG_LOGGER_NAME, SKILL_SLUG
from util.logging_config import get_skill_logger, setup_skill_logging
def _cli_str_or_none(raw: Optional[str]) -> Optional[str]:
if raw is None:
return None
v = str(raw).strip()
return v or None
def _handle_run(args: argparse.Namespace) -> int:
tail = [str(x).strip() for x in (args.run_tail or []) if str(x).strip()]
if len(tail) > 2:
print("参数过多。")
print("用法:python main.py run [target [input_id]] | run [-t target] [-i input_id]")
return 1
t_target: Optional[str] = None
t_input: Optional[str] = None
if len(tail) == 2:
t_target, t_input = tail[0], tail[1]
elif len(tail) == 1:
if tail[0].isdigit():
t_input = tail[0]
else:
t_target = tail[0]
pick_t = _cli_str_or_none(getattr(args, "target", None))
pick_i = _cli_str_or_none(getattr(args, "input_id", None))
target = pick_t or t_target
input_id = pick_i or t_input
return cmd_run(target=target, input_id=input_id)
def _handle_calculate_simple(args: argparse.Namespace) -> int:
base_salary = float(args.base_salary)
period = _cli_str_or_none(args.period) or ""
reported_net = _parse_float_or_none(_cli_str_or_none(args.reported_net))
region = _cli_str_or_none(args.region) or "default"
return cmd_calculate_simple(base_salary=base_salary, period=period, reported_net=reported_net, region=region)
def _handle_calculate_full(args: argparse.Namespace) -> int:
region = _cli_str_or_none(args.region) or "default"
period = _cli_str_or_none(args.period) or ""
raw_payload = _cli_str_or_none(args.raw_payload) or ""
return cmd_calculate_full(
base_salary=float(args.base_salary or 0),
performance=float(args.performance or 0),
full_attendance=float(args.full_attendance or 0),
meal_allowance=float(args.meal_allowance or 0),
transport_allowance=float(args.transport_allowance or 0),
overtime_pay=float(args.overtime_pay or 0),
other_gross=float(args.other_gross or 0),
social_personal=_parse_float_or_none(_cli_str_or_none(args.social_personal)),
housing_fund_personal=_parse_float_or_none(_cli_str_or_none(args.housing_fund_personal)),
tax=_parse_float_or_none(_cli_str_or_none(args.tax)),
leave_deduction=float(args.leave_deduction or 0),
other_deduction=float(args.other_deduction or 0),
period=period,
reported_net=_parse_float_or_none(_cli_str_or_none(args.reported_net)),
raw_payload=raw_payload,
region=region,
)
def _handle_explain(args: argparse.Namespace) -> int:
term = _cli_str_or_none(args.term) or ""
return cmd_explain(term)
def _handle_view_config(args: argparse.Namespace) -> int:
region = _cli_str_or_none(args.region)
return cmd_view_config(region=region)
def _handle_edit_config(args: argparse.Namespace) -> int:
return cmd_edit_config(
config_key=_cli_str_or_none(args.config_key) or "",
region=_cli_str_or_none(args.region) or "default",
config_value=float(args.config_value),
)
def _print_full_usage() -> None:
print("工资查询助手(main.py)可用命令:")
print(" python main.py calculate-simple --base-salary N --period YYYY-MM [--reported-net N] [--region default|beijing|shanghai|guangzhou]")
print(" python main.py calculate-full [全字段参数] --period YYYY-MM [--region ...]")
print(" python main.py explain --term 名词")
print(" python main.py view-config [--region ...]")
print(" python main.py edit-config --config-key KEY --region R --config-value V")
print(" python main.py run [target [input_id]] [-t target] [-i input_id]")
print(" python main.py health")
print(" python main.py config-path")
print(" python main.py version")
print(" python main.py init-db")
def build_parser() -> ZhArgumentParser:
p = ZhArgumentParser(
prog="main.py",
description="工资查询助手:核算应发/扣除/实发,核对差异,解释名词,估算个税。",
formatter_class=argparse.RawDescriptionHelpFormatter,
)
sub = p.add_subparsers(dest="cmd", required=True, parser_class=ZhArgumentParser)
# calculate-simple
sp = sub.add_parser("calculate-simple", help="快速估算(仅基本工资+period)")
sp.add_argument("--base-salary", required=True, type=float, metavar="基本工资")
sp.add_argument("--period", required=True, metavar="YYYY-MM")
sp.add_argument("--reported-net", default=None, metavar="用户自报实发")
sp.add_argument("--region", default=None, metavar="地区")
sp.set_defaults(handler=_handle_calculate_simple)
# calculate-full
sp = sub.add_parser("calculate-full", help="精确核算(全字段)")
sp.add_argument("--base-salary", type=float, default=0, metavar="基本工资")
sp.add_argument("--performance", type=float, default=0, metavar="绩效")
sp.add_argument("--full-attendance", type=float, default=0, metavar="全勤", dest="full_attendance")
sp.add_argument("--meal-allowance", type=float, default=0, metavar="餐补", dest="meal_allowance")
sp.add_argument("--transport-allowance", type=float, default=0, metavar="交通补", dest="transport_allowance")
sp.add_argument("--overtime-pay", type=float, default=0, metavar="加班费", dest="overtime_pay")
sp.add_argument("--other-gross", type=float, default=0, metavar="其他应发", dest="other_gross")
sp.add_argument("--social-personal", default=None, metavar="社保个人", dest="social_personal")
sp.add_argument("--housing-fund-personal", default=None, metavar="公积金个人", dest="housing_fund_personal")
sp.add_argument("--tax", default=None, metavar="个税")
sp.add_argument("--leave-deduction", type=float, default=0, metavar="事假扣款", dest="leave_deduction")
sp.add_argument("--other-deduction", type=float, default=0, metavar="其他扣款", dest="other_deduction")
sp.add_argument("--period", default="", metavar="YYYY-MM")
sp.add_argument("--reported-net", default=None, metavar="用户自报实发", dest="reported_net")
sp.add_argument("--raw-payload", default=None, metavar="原始工资条文本", dest="raw_payload")
sp.add_argument("--region", default=None, metavar="地区")
sp.set_defaults(handler=_handle_calculate_full)
# explain
sp = sub.add_parser("explain", help="名词解释")
sp.add_argument("--term", required=True, metavar="名词")
sp.set_defaults(handler=_handle_explain)
# view-config
sp = sub.add_parser("view-config", help="查看费率配置")
sp.add_argument("--region", default=None, metavar="地区")
sp.set_defaults(handler=_handle_view_config)
# edit-config
sp = sub.add_parser("edit-config", help="编辑费率配置")
sp.add_argument("--config-key", required=True, metavar="配置键", dest="config_key")
sp.add_argument("--region", default="default", metavar="地区")
sp.add_argument("--config-value", required=True, type=float, metavar="新值", dest="config_value")
sp.set_defaults(handler=_handle_edit_config)
sp = sub.add_parser("run", help="任务执行命令骨架")
sp.add_argument("--target", "-t", default=None, metavar="目标")
sp.add_argument("--input-id", "-i", default=None, metavar="输入id", dest="input_id")
sp.add_argument("run_tail", nargs="*", metavar="位置参数")
sp.set_defaults(handler=_handle_run)
sp = sub.add_parser("health", help="健康检查")
sp.set_defaults(handler=lambda _a: cmd_health())
sp = sub.add_parser("config-path", help="输出用户 .env 与模板路径")
sp.set_defaults(handler=lambda _a: cmd_config_path())
sp = sub.add_parser("version", help="版本信息(JSON)")
sp.set_defaults(handler=lambda _a: cmd_version())
sp = sub.add_parser("init-db", help="幂等初始化本地数据库与数据管理元数据")
sp.set_defaults(handler=lambda _a: cmd_init_db())
return p
def main(argv: Optional[List[str]] = None) -> int:
argv = argv if argv is not None else sys.argv[1:]
bootstrap_skill_config()
setup_skill_logging(SKILL_SLUG, LOG_LOGGER_NAME)
get_skill_logger().info("cli_start argv=%s", sys.argv)
if not argv:
_print_full_usage()
return 1
parser = build_parser()
args = parser.parse_args(argv)
return int(args.handler(args))