补充业务详情指导成本入口、来源版本和历史封存保护,接入月份锁并完善权限复核。 同步接入读取服务运行状态到页面,补充前端控制、回归测试和服务端只读连接授权校验。 定向隔离测试:89 项通过,1 条既有 Starlette 弃用警告。
69 lines
2.8 KiB
Python
69 lines
2.8 KiB
Python
"""月份行锁同时保护工资生成与分配事实修改。"""
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
|
|
from sqlalchemy import select
|
|
from sqlalchemy.dialects.mysql import insert
|
|
|
|
from app.core.exceptions import BadRequestError, ForbiddenError
|
|
from app.core.month import validate_month
|
|
from app.models import MonthlySettlement, PayrollMonth
|
|
from app.services.payroll_access import ensure_payroll_writer
|
|
|
|
|
|
def current_month() -> str:
|
|
return datetime.now(timezone(timedelta(hours=8))).strftime("%Y-%m")
|
|
|
|
|
|
def month_state(session, month):
|
|
"""查询生成锁及制作人,兼容升级前已存在的月结记录。"""
|
|
validate_month(month)
|
|
control = session.scalar(select(PayrollMonth).where(PayrollMonth.month == month))
|
|
existing = session.scalar(
|
|
select(MonthlySettlement.id).where(MonthlySettlement.settlement_month == month)
|
|
)
|
|
return {
|
|
"month": month,
|
|
"allocation_locked": bool(existing or (control and control.generated_at)),
|
|
"maker_name": control.maker_name if control else None,
|
|
}
|
|
|
|
|
|
def lock_month_row(session, month, operator):
|
|
"""以月份唯一键串行化首次建行及后续操作,不改变已有制作人。"""
|
|
validate_month(month)
|
|
values = {"month": month, "maker_id": operator.id, "maker_name": operator.username}
|
|
if session.bind.dialect.name == "mysql":
|
|
stmt = insert(PayrollMonth).values(**values)
|
|
session.execute(stmt.on_duplicate_key_update(month=stmt.inserted.month))
|
|
elif session.scalar(select(PayrollMonth.id).where(PayrollMonth.month == month)) is None:
|
|
session.add(PayrollMonth(**values))
|
|
session.flush()
|
|
control = session.scalar(
|
|
select(PayrollMonth).where(PayrollMonth.month == month).with_for_update()
|
|
.execution_options(populate_existing=True)
|
|
)
|
|
return control
|
|
|
|
|
|
def lock_month(session, month, operator, *, require_maker=True):
|
|
if require_maker:
|
|
ensure_payroll_writer(operator)
|
|
control = lock_month_row(session, month, operator)
|
|
if control.generated_at is not None and control.maker_id != operator.id:
|
|
raise ForbiddenError("该月工资由其他制作人生成,不可代为修改或重新生成")
|
|
if control.generated_at is None:
|
|
control.maker_id, control.maker_name = operator.id, operator.username
|
|
return control
|
|
|
|
|
|
def ensure_allocation_editable(session, month, operator, *, require_maker=True):
|
|
control = lock_month(session, month, operator, require_maker=require_maker)
|
|
# 兼容升级前已有月结;没有控制行不代表历史工资可以修改。
|
|
old = session.scalar(
|
|
select(MonthlySettlement.id).where(MonthlySettlement.settlement_month == month)
|
|
)
|
|
if control.generated_at is not None or old is not None:
|
|
raise BadRequestError("该月工资已生成,二次分配已锁定;差额请走当月调整单")
|
|
return control
|