"""月份行锁同时保护工资生成与分配事实修改。""" 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(session, month, operator, *, require_maker=True): """INSERT 的唯一键串行化首次建行,避免两个请求同时认为月份不存在。""" if require_maker: ensure_payroll_writer(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() ) 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