| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245 |
- <?php
- /**
- * AC-15 阶段 1 · oracle 侧探针(由 Go runner 通过 embed 释放到临时目录后执行)
- *
- * 职责:在 legacy 应用里真实调用一次结课结算,把"异常前已写入的库值"读回来,以 JSON 输出。
- * 纪律:
- * - 只在 legacy_oracle / legacy_finance 上写;finance_bk 一律拒绝
- * - 整个结算过程包在一个事务里,finally 必定 rollBack
- * - 每次进程独立 cachePath ⇒ 缓存不跨 case,保证幂等
- * - 口令只从环境变量读,不打印
- *
- * 入参(全部环境变量):
- * AC15_DB_HOST / AC15_DB_PORT / AC15_DB_NAME / AC15_DB_USER / AC15_DB_PASS
- * AC15_LEGACY_REPO legacy 应用根目录
- * AC15_RECORD_ID 结课记录 id(varchar(32))
- * AC15_IS_SUPPLEMENT "0"/"1"
- * AC15_FROZEN_EPOCH 冻结时钟的 epoch(整秒,字符串)
- * AC15_MUTATIONS JSON 数组:事务内、结算前要执行的 SQL(合成 case 造数用;空数组表示不改)
- *
- * 出参:stdout 一个 JSON 对象(不夹带任何调试噪声)
- */
- $repo = getenv('AC15_LEGACY_REPO') ?: 'E:/study/php/ysw-finance/yuansiwei-finance';
- $host = getenv('AC15_DB_HOST') ?: '127.0.0.1';
- $port = (int) (getenv('AC15_DB_PORT') ?: 3306);
- $name = getenv('AC15_DB_NAME') ?: 'legacy_oracle';
- $user = getenv('AC15_DB_USER') ?: 'dev_legacy';
- $pass = getenv('AC15_DB_PASS');
- $recordId = getenv('AC15_RECORD_ID');
- $isSupplement = getenv('AC15_IS_SUPPLEMENT') === '1';
- $frozenEpoch = (int) (getenv('AC15_FROZEN_EPOCH') ?: 0);
- $mutations = json_decode(getenv('AC15_MUTATIONS') ?: '[]', true) ?: [];
- function out(array $payload): void
- {
- $payload['harness'] = 'ac15-stage1-oracle';
- echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PARTIAL_OUTPUT_ON_ERROR);
- exit(0);
- }
- function bail(string $code, string $detail): void
- {
- out(['ok' => false, 'error' => $code, 'errorDetail' => $detail]);
- }
- // ---- 硬守卫:库名 + host 白名单(拒绝 finance_bk 与任何远端 host)----
- if (!in_array($host, ['127.0.0.1', 'localhost', '::1'], true)) {
- bail('GUARD_HOST', 'host not in allow-list');
- }
- if (!in_array($name, ['legacy_oracle', 'legacy_finance'], true)) {
- bail('GUARD_DB', 'write target must be legacy_oracle / legacy_finance, got ' . $name);
- }
- if ($pass === false || $pass === '') {
- bail('NO_CREDENTIAL', 'AC15_DB_PASS missing');
- }
- if (empty($recordId)) {
- bail('NO_RECORD', 'AC15_RECORD_ID missing');
- }
- define('YII_DEBUG', false);
- define('YII_ENV', 'prod');
- require $repo . '/vendor/autoload.php';
- require $repo . '/vendor/yiisoft/yii2/Yii.php';
- $config = require $repo . '/config/console.php';
- $localDb = [
- 'class' => 'yii\db\Connection',
- 'dsn' => sprintf('mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4', $host, $port, $name),
- 'username' => $user,
- 'password' => $pass,
- 'charset' => 'utf8mb4',
- 'tablePrefix' => 't_',
- 'enableSchemaCache' => false,
- ];
- $config['components']['db'] = $localDb;
- $config['components']['dbFinance'] = $localDb;
- // 独立 cachePath:保证每次进程冷缓存,重复执行结论一致(幂等)
- $cachePath = rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR . 'ac15-cache-' . getmypid();
- $config['components']['cache'] = ['class' => 'yii\caching\FileCache', 'cachePath' => $cachePath];
- try {
- $app = new yii\console\Application($config);
- } catch (Throwable $e) {
- bail('BOOT_FAIL', $e->getMessage());
- }
- use app\modules\courses\helpers\CoursesHelper;
- use app\modules\courses\helpers\CoursesRecordHelper;
- use app\modules\courses\helpers\CoursePaymentDetailHelper;
- use app\modules\user\helpers\TeacherInformationHelper;
- $db = Yii::$app->dbFinance;
- $clock = [];
- try {
- // ---- 冻结时钟(会话级)+ 时区统一 ----
- if ($frozenEpoch > 0) {
- $db->createCommand('SET SESSION time_zone = :tz', [':tz' => '+08:00'])->execute();
- $db->createCommand('SET SESSION timestamp = :ts', [':ts' => (string) $frozenEpoch])->execute();
- $clock = [
- 'frozenEpoch' => $frozenEpoch,
- 'now' => (string) $db->createCommand('SELECT NOW()')->queryScalar(),
- 'unixNow' => (string) $db->createCommand('SELECT UNIX_TIMESTAMP()')->queryScalar(),
- 'phpTime' => time(),
- 'phpDate' => date('Y-m-d H:i:s'),
- 'timeZone' => (string) $db->createCommand('SELECT @@session.time_zone')->queryScalar(),
- ];
- }
- $rec = $db->createCommand(
- "SELECT id, schoolid, campusid, teacherid, coursesid, classid, teacher_reward_type,
- student_number, status, income, teacher_reward,
- DATE_FORMAT(actual_start_time,'%Y-%m-%d %H:%i:%s') st,
- DATE_FORMAT(actual_end_time,'%Y-%m-%d %H:%i:%s') et
- FROM t_courses_record WHERE id=:id", [':id' => $recordId])->queryOne();
- if (!$rec) {
- bail('RECORD_NOT_FOUND', $recordId);
- }
- $classifyid = CoursesHelper::getClassifyidById($rec['coursesid']);
- $teacherExp = TeacherInformationHelper::getTeacherExp($rec['teacherid']);
- $income = round((float) $db->createCommand(
- "SELECT IFNULL(SUM(cost),0) FROM t_courses_record_student WHERE recordid=:r", [':r' => $recordId])->queryScalar(), 3);
- $studentsBefore = $db->createCommand(
- "SELECT studentid, type, cost, teacher_reward FROM t_courses_record_student WHERE recordid=:r ORDER BY studentid, type",
- [':r' => $recordId])->queryAll();
- $eligible = 0;
- foreach ($studentsBefore as $s) {
- if ((int) $s['type'] === 1 || (int) $s['type'] === 2) {
- $eligible++;
- }
- }
- // SP 命中的原始规则(绕过 helper 缓存,单独取一次,作为"路径证据"的 detail_id 来源)
- $quote = null;
- try {
- $quote = CoursePaymentDetailHelper::getTeacherPaymentDetailStoredProcedure(
- $rec['teacherid'], $rec['schoolid'], $rec['campusid'], $classifyid, $rec['st']
- );
- } catch (Throwable $e) {
- $quote = 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();
- $exception = null;
- $mutationErrors = [];
- $ret = null;
- $after = null;
- $studentsAfter = [];
- try {
- foreach ($mutations as $sql) {
- try {
- $db->createCommand($sql)->execute();
- } catch (Throwable $e) {
- $mutationErrors[] = ['sql' => $sql, 'error' => $e->getMessage()];
- }
- }
- try {
- $ret = CoursesRecordHelper::settlementExpenses($recordId, $isSupplement);
- } catch (Throwable $e) {
- $exception = [
- 'class' => get_class($e),
- 'message' => $e->getMessage(),
- 'file' => $e->getFile(),
- 'line' => $e->getLine(),
- ];
- }
- // A24:它必抛异常,所以要读"异常前已写入"的库值
- $after = $db->createCommand(
- "SELECT status, income, teacher_reward, student_number,
- DATE_FORMAT(actual_end_time,'%Y-%m-%d %H:%i:%s') aet
- FROM t_courses_record WHERE id=:id", [':id' => $recordId])->queryOne();
- $studentsAfter = $db->createCommand(
- "SELECT studentid, type, cost, teacher_reward FROM t_courses_record_student WHERE recordid=:r ORDER BY studentid, type",
- [':r' => $recordId])->queryAll();
- } finally {
- if ($tx !== null && $tx->getIsActive()) {
- $tx->rollBack();
- }
- }
- // 回滚后复核(证明零残留)
- $studentsRestored = $db->createCommand(
- "SELECT studentid, type, cost, teacher_reward FROM t_courses_record_student WHERE recordid=:r ORDER BY studentid, type",
- [':r' => $recordId])->queryAll();
- $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));
- out([
- 'ok' => true,
- 'recordId' => $recordId,
- 'isSupplement' => $isSupplement,
- '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) $rec['teacher_reward_type'],
- 'studentNumber' => (int) $rec['student_number'],
- 'eligibleCount' => $eligible,
- 'incomeSumCost3' => $income,
- 'statusBefore' => (int) $rec['status'],
- 'teacherRewardBefore' => (float) $rec['teacher_reward'],
- 'actualStartTime' => $rec['st'],
- ],
- 'quote' => $quoteOut,
- 'clock' => $clock,
- 'mutations' => $mutations,
- 'mutationErrors' => $mutationErrors,
- 'return' => $ret,
- 'exception' => $exception,
- 'after' => $after === null ? null : [
- 'status' => (int) $after['status'],
- 'income' => (float) $after['income'],
- 'teacherReward' => (float) $after['teacher_reward'],
- 'studentNumber' => (int) $after['student_number'],
- 'actualEndTime' => $after['aet'],
- ],
- 'studentsBefore' => array_map(static function ($r) {
- return ['studentid' => (int) $r['studentid'], 'type' => (int) $r['type'],
- 'cost' => (float) $r['cost'], 'teacherReward' => (float) $r['teacher_reward']];
- }, $studentsBefore),
- 'studentsAfter' => array_map(static function ($r) {
- return ['studentid' => (int) $r['studentid'], 'type' => (int) $r['type'],
- 'cost' => (float) $r['cost'], 'teacherReward' => (float) $r['teacher_reward']];
- }, $studentsAfter),
- 'rollbackRestored' => $restored,
- ]);
- } catch (Throwable $e) {
- bail('UNEXPECTED', $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine());
- }
|