|
@@ -34,6 +34,8 @@ $mutations = json_decode(getenv('AC15_MUTATIONS') ?: '[]', true) ?: [];
|
|
|
function out(array $payload): void
|
|
function out(array $payload): void
|
|
|
{
|
|
{
|
|
|
$payload['harness'] = 'ac15-stage1-oracle';
|
|
$payload['harness'] = 'ac15-stage1-oracle';
|
|
|
|
|
+ // 输出标记:PHP 的 notice/warning 可能混进 stdout,Go 侧靠这个标记定位 JSON 起点
|
|
|
|
|
+ echo "##AC15_JSON_BEGIN##";
|
|
|
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR);
|
|
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR);
|
|
|
exit(0);
|
|
exit(0);
|
|
|
}
|
|
}
|
|
@@ -95,8 +97,11 @@ $clock = [];
|
|
|
try {
|
|
try {
|
|
|
// ---- 冻结时钟(会话级)+ 时区统一 ----
|
|
// ---- 冻结时钟(会话级)+ 时区统一 ----
|
|
|
if ($frozenEpoch > 0) {
|
|
if ($frozenEpoch > 0) {
|
|
|
- $db->createCommand('SET SESSION time_zone = :tz', [':tz' => '+08:00'])->execute();
|
|
|
|
|
- $db->createCommand('SET SESSION timestamp = :ts', [':ts' => (string) $frozenEpoch])->execute();
|
|
|
|
|
|
|
+ // 注意:timestamp 是数值型会话变量,pdO 绑定成字符串会报
|
|
|
|
|
+ // `1232 Incorrect argument type to variable 'timestamp'`
|
|
|
|
|
+ // 所以这两个都用整数/字面量直接拼(值来源已强制转 int/固定常量,无注入面)
|
|
|
|
|
+ $db->createCommand("SET SESSION time_zone = '+08:00'")->execute();
|
|
|
|
|
+ $db->createCommand('SET SESSION timestamp = ' . (int) $frozenEpoch)->execute();
|
|
|
$clock = [
|
|
$clock = [
|
|
|
'frozenEpoch' => $frozenEpoch,
|
|
'frozenEpoch' => $frozenEpoch,
|
|
|
'now' => (string) $db->createCommand('SELECT NOW()')->queryScalar(),
|
|
'now' => (string) $db->createCommand('SELECT NOW()')->queryScalar(),
|
|
@@ -131,34 +136,20 @@ try {
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // SP 命中的原始规则(绕过 helper 缓存,单独取一次,作为"路径证据"的 detail_id 来源)
|
|
|
|
|
|
|
+ // SP 命中的原始规则:**必须在事务内、造数之后读**(否则拿到的是造数前的旧规则,
|
|
|
|
|
+ // 会造成"PHP 用新规则、target 用旧规则"的假 DEFECT)
|
|
|
$quote = null;
|
|
$quote = null;
|
|
|
- try {
|
|
|
|
|
- $quote = CoursePaymentDetailHelper::getTeacherPaymentDetailStoredProcedure(
|
|
|
|
|
- $rec['teacherid'], $rec['schoolid'], $rec['campusid'], $classifyid, $rec['st']
|
|
|
|
|
- );
|
|
|
|
|
- } catch (Throwable $e) {
|
|
|
|
|
- $quote = null;
|
|
|
|
|
- }
|
|
|
|
|
$quoteOut = null;
|
|
$quoteOut = null;
|
|
|
- if (is_array($quote)) {
|
|
|
|
|
- $quoteOut = [];
|
|
|
|
|
- foreach (['id', 'course_paymentid', 'gradeid', 'subjectid', 'class_type', 'classifyid',
|
|
|
|
|
- 'percentage', 'min_payment', 'max_payment', 'reward', 'teacher_cert',
|
|
|
|
|
- 'teacher_exp_min', 'teacher_exp_max'] as $k) {
|
|
|
|
|
- if (array_key_exists($k, $quote)) {
|
|
|
|
|
- $quoteOut[$k] = (float) $quote[$k];
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- // ---- 事务内:造数 → 结算 → 读回 → 必定回滚 ----
|
|
|
|
|
|
|
+ // ---- 事务内:造数 → **重读权威输入** → 结算 → 读回 → 必定回滚 ----
|
|
|
$tx = $db->beginTransaction();
|
|
$tx = $db->beginTransaction();
|
|
|
$exception = null;
|
|
$exception = null;
|
|
|
$mutationErrors = [];
|
|
$mutationErrors = [];
|
|
|
$ret = null;
|
|
$ret = null;
|
|
|
$after = null;
|
|
$after = null;
|
|
|
$studentsAfter = [];
|
|
$studentsAfter = [];
|
|
|
|
|
+ $inputs = null;
|
|
|
|
|
+ $studentsEffective = $studentsBefore;
|
|
|
try {
|
|
try {
|
|
|
foreach ($mutations as $sql) {
|
|
foreach ($mutations as $sql) {
|
|
|
try {
|
|
try {
|
|
@@ -167,6 +158,56 @@ try {
|
|
|
$mutationErrors[] = ['sql' => $sql, 'error' => $e->getMessage()];
|
|
$mutationErrors[] = ['sql' => $sql, 'error' => $e->getMessage()];
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ // 造数会改 cost / teacher_reward_type 等,**必须重读**:PHP 结算用的是造数之后的世界,
|
|
|
|
|
+ // target 侧若用造数之前的值,就会出现"基数不同"的假 DEFECT。
|
|
|
|
|
+ $rec2 = $db->createCommand(
|
|
|
|
|
+ "SELECT teacher_reward_type, student_number, status, income, teacher_reward
|
|
|
|
|
+ FROM t_courses_record WHERE id=:id", [':id' => $recordId])->queryOne();
|
|
|
|
|
+ $studentsEffective = $db->createCommand(
|
|
|
|
|
+ "SELECT studentid, type, cost, teacher_reward FROM t_courses_record_student WHERE recordid=:r ORDER BY studentid, type",
|
|
|
|
|
+ [':r' => $recordId])->queryAll();
|
|
|
|
|
+ $incomeEffective = round((float) $db->createCommand(
|
|
|
|
|
+ "SELECT IFNULL(SUM(cost),0) FROM t_courses_record_student WHERE recordid=:r", [':r' => $recordId])->queryScalar(), 3);
|
|
|
|
|
+ $eligibleEffective = 0;
|
|
|
|
|
+ foreach ($studentsEffective as $s) {
|
|
|
|
|
+ if ((int) $s['type'] === 1 || (int) $s['type'] === 2) {
|
|
|
|
|
+ $eligibleEffective++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ // 读规则(此时造数已生效,SP 会看到注入的规则)
|
|
|
|
|
+ try {
|
|
|
|
|
+ $quote = CoursePaymentDetailHelper::getTeacherPaymentDetailStoredProcedure(
|
|
|
|
|
+ $rec['teacherid'], $rec['schoolid'], $rec['campusid'], $classifyid, $rec['st']
|
|
|
|
|
+ );
|
|
|
|
|
+ } catch (Throwable $e) {
|
|
|
|
|
+ $quote = null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (is_array($quote)) {
|
|
|
|
|
+ $quoteOut = [];
|
|
|
|
|
+ foreach (['id', 'course_paymentid', 'gradeid', 'subjectid', 'class_type', 'classifyid',
|
|
|
|
|
+ 'percentage', 'min_payment', 'max_payment', 'reward', 'teacher_cert',
|
|
|
|
|
+ 'teacher_exp_min', 'teacher_exp_max'] as $k) {
|
|
|
|
|
+ if (array_key_exists($k, $quote)) {
|
|
|
|
|
+ $quoteOut[$k] = (float) $quote[$k];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $inputs = [
|
|
|
|
|
+ 'schoolid' => (int) $rec['schoolid'],
|
|
|
|
|
+ 'campusid' => (int) $rec['campusid'],
|
|
|
|
|
+ 'teacherid' => (int) $rec['teacherid'],
|
|
|
|
|
+ 'coursesid' => (int) $rec['coursesid'],
|
|
|
|
|
+ 'classifyid' => (int) $classifyid,
|
|
|
|
|
+ 'teacherExp' => (float) $teacherExp,
|
|
|
|
|
+ 'teacherRewardType' => (int) $rec2['teacher_reward_type'],
|
|
|
|
|
+ 'studentNumber' => (int) $rec2['student_number'],
|
|
|
|
|
+ 'eligibleCount' => $eligibleEffective,
|
|
|
|
|
+ 'incomeSumCost3' => $incomeEffective,
|
|
|
|
|
+ 'statusBefore' => (int) $rec2['status'],
|
|
|
|
|
+ 'teacherRewardBefore' => (float) $rec2['teacher_reward'],
|
|
|
|
|
+ 'actualStartTime' => $rec['st'],
|
|
|
|
|
+ ];
|
|
|
try {
|
|
try {
|
|
|
$ret = CoursesRecordHelper::settlementExpenses($recordId, $isSupplement);
|
|
$ret = CoursesRecordHelper::settlementExpenses($recordId, $isSupplement);
|
|
|
} catch (Throwable $e) {
|
|
} catch (Throwable $e) {
|
|
@@ -198,11 +239,27 @@ try {
|
|
|
$restored = ($db->createCommand("SELECT teacher_reward FROM t_courses_record WHERE id=:id", [':id' => $recordId])->queryScalar() == $rec['teacher_reward'])
|
|
$restored = ($db->createCommand("SELECT teacher_reward FROM t_courses_record WHERE id=:id", [':id' => $recordId])->queryScalar() == $rec['teacher_reward'])
|
|
|
&& (json_encode($studentsRestored) === json_encode($studentsBefore));
|
|
&& (json_encode($studentsRestored) === json_encode($studentsBefore));
|
|
|
|
|
|
|
|
|
|
+ // PHP 是否真的重写了逐行(recount 分支是否进入):只有重写过,逐行比对才有意义
|
|
|
|
|
+ $perRowRewritten = false;
|
|
|
|
|
+ $effMap = [];
|
|
|
|
|
+ foreach ($studentsEffective as $s) {
|
|
|
|
|
+ $effMap[(int) $s['studentid']] = (float) $s['teacher_reward'];
|
|
|
|
|
+ }
|
|
|
|
|
+ foreach ($studentsAfter as $s) {
|
|
|
|
|
+ $sid = (int) $s['studentid'];
|
|
|
|
|
+ if (isset($effMap[$sid]) && abs($effMap[$sid] - (float) $s['teacher_reward']) > 0.0005) {
|
|
|
|
|
+ $perRowRewritten = true;
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
out([
|
|
out([
|
|
|
'ok' => true,
|
|
'ok' => true,
|
|
|
'recordId' => $recordId,
|
|
'recordId' => $recordId,
|
|
|
'isSupplement' => $isSupplement,
|
|
'isSupplement' => $isSupplement,
|
|
|
- 'inputs' => [
|
|
|
|
|
|
|
+ 'perRowRewritten' => $perRowRewritten,
|
|
|
|
|
+ 'inputsEffective' => $inputs,
|
|
|
|
|
+ 'inputsBeforeMutation' => [
|
|
|
'schoolid' => (int) $rec['schoolid'],
|
|
'schoolid' => (int) $rec['schoolid'],
|
|
|
'campusid' => (int) $rec['campusid'],
|
|
'campusid' => (int) $rec['campusid'],
|
|
|
'teacherid' => (int) $rec['teacherid'],
|
|
'teacherid' => (int) $rec['teacherid'],
|
|
@@ -230,6 +287,10 @@ try {
|
|
|
'studentNumber' => (int) $after['student_number'],
|
|
'studentNumber' => (int) $after['student_number'],
|
|
|
'actualEndTime' => $after['aet'],
|
|
'actualEndTime' => $after['aet'],
|
|
|
],
|
|
],
|
|
|
|
|
+ 'studentsEffective' => array_map(static function ($r) {
|
|
|
|
|
+ return ['studentid' => (int) $r['studentid'], 'type' => (int) $r['type'],
|
|
|
|
|
+ 'cost' => (float) $r['cost'], 'teacherReward' => (float) $r['teacher_reward']];
|
|
|
|
|
+ }, $studentsEffective),
|
|
|
'studentsBefore' => array_map(static function ($r) {
|
|
'studentsBefore' => array_map(static function ($r) {
|
|
|
return ['studentid' => (int) $r['studentid'], 'type' => (int) $r['type'],
|
|
return ['studentid' => (int) $r['studentid'], 'type' => (int) $r['type'],
|
|
|
'cost' => (float) $r['cost'], 'teacherReward' => (float) $r['teacher_reward']];
|
|
'cost' => (float) $r['cost'], 'teacherReward' => (float) $r['teacher_reward']];
|