readMultiSheetExcel($file_name); if (!$workbook) { return array(false, '无法读取Excel文件或文件格式不正确', array()); } // 检查结算表 $settlement_sheet = $workbook['settlement']; $settlement_headers = $this->getSheetHeaders($settlement_sheet); $required_fields = array('订单编号', '单据编号', '费用项', '金额'); foreach ($required_fields as $field) { if (!in_array($field, $settlement_headers)) { return array(false, '结算表缺少必填字段:' . $field, array()); } } // 检查资金表 $fund_sheet = $workbook['fund']; $fund_headers = $this->getSheetHeaders($fund_sheet); $required_fund_fields = array('商户号', '日期', '商户订单号'); foreach ($required_fund_fields as $field) { if (!in_array($field, $fund_headers)) { return array(false, '资金表缺少必填字段:' . $field, array()); } } return array(true, '文件验证通过', $settlement_sheet[0]); } catch (Exception $e) { return array(false, '文件检查异常:' . $e->getMessage(), array()); } } /** * 读取多工作表Excel文件 * @param string $file_name 文件名 * @return array|false */ private function readMultiSheetExcel($file_name) { try { // 检查Vtiful\Kernel\Excel是否可用 if (!class_exists('Vtiful\Kernel\Excel')) { throw new Exception('Vtiful\Kernel\Excel库未安装'); } $path = pathinfo($file_name); $excel = new \Vtiful\Kernel\Excel(['path' => $path['dirname']]); // 读取结算表 $settlement_data = $this->readSheetData($excel, $path['basename'], '结算表'); if (!$settlement_data) { throw new Exception('无法读取结算表数据'); } // 读取资金表 $fund_data = $this->readSheetData($excel, $path['basename'], '资金表'); if (!$fund_data) { throw new Exception('无法读取资金表数据'); } return array( 'settlement' => $settlement_data, 'fund' => $fund_data ); } catch (Exception $e) { return false; } } /** * 读取指定工作表数据 * @param object $excel Excel对象 * @param string $filename 文件名 * @param string $sheet_name 工作表名 * @return array|false */ private function readSheetData($excel, $filename, $sheet_name) { try { // 打开文件并切换到指定工作表 $excel->openFile($filename); // 获取所有工作表名称 $sheet_names = $excel->sheetList(); if (!in_array($sheet_name, $sheet_names)) { return false; } // 切换到指定工作表 $excel->openSheet($sheet_name); // 读取标题行 $title = $excel->nextRow(); if (!$title) { return false; } // 设置日期列类型转换 $type = array_pad([], count($title), \Vtiful\Kernel\Excel::TYPE_STRING); // 根据工作表类型设置不同的日期字段 if ($sheet_name === '结算表') { $timeColumn = array('费用结算时间', '费用计费时间', '费用发生时间'); } else if ($sheet_name === '资金表') { $timeColumn = array('日期', '账单日期'); } else { $timeColumn = array(); } foreach ($timeColumn as $v) { if ($k = array_search($v, $title)) { $type[$k] = \Vtiful\Kernel\Excel::TYPE_TIMESTAMP; } } $excel->setType($type); // 读取所有数据 $data = array(); $data[] = $title; // 第一行是标题 while (($row = $excel->nextRow()) !== null) { // 转换日期字段 foreach ($timeColumn as $v) { if ($k = array_search($v, $title)) { if (isset($row[$k]) && is_numeric($row[$k]) && $row[$k] > 0) { $timestamp = ($row[$k] - 25569) * 86400; // Excel基准日期是1900-01-01,Unix基准是1970-01-01 $row[$k] = date('Y-m-d H:i:s', $timestamp); } } } // 处理金额字段精度(解决浮点数精度问题) if ($sheet_name === '结算表') { $amountColumn = array('金额'); } else if ($sheet_name === '资金表') { $amountColumn = array('账户余额(元)', '收入金额(元)', '支出金额(元)'); } else { $amountColumn = array(); } foreach ($amountColumn as $v) { if ($k = array_search($v, $title)) { if (isset($row[$k]) && is_numeric($row[$k])) { // 保留2位小数,避免浮点数精度问题 $row[$k] = number_format((float)$row[$k], 2, '.', ''); } } } $data[] = $row; } return $data; } catch (Exception $e) { return false; } } /** * 获取工作表标题行 * @param array $sheet_data 工作表数据 * @return array */ private function getSheetHeaders($sheet_data) { if (empty($sheet_data) || !is_array($sheet_data)) { return array(); } // 第一行就是标题行 $headers = $sheet_data[0]; return array_map('trim', $headers); } /** * 重写process方法,设置工作表类型后调用父类方法,并处理状态更新 * @param int $cursor_id 游标ID * @param array $params 参数 * @param array $errmsg 错误信息 * @return bool */ public function process($cursor_id, $params, &$errmsg) { // 设置工作表类型,供getSdf方法使用 $this->sheet_type = $params['sheet_type'] ?? 'settlement'; // 调用父类的process方法 $result = parent::process($cursor_id, $params, $errmsg); // 京东钱包导入特有的状态更新逻辑 if (isset($params['import_id'])) { $db = kernel::database(); $import_id = intval($params['import_id']); if ($result) { // 处理成功,更新对应工作表的完成分片数 $sheet_type = $params['sheet_type'] ?? 'settlement'; if ($sheet_type === 'settlement') { // 结算表分片完成,增加完成计数 $sql = "UPDATE sdb_financebase_bill_import_jdwallet SET settlement_chunks_completed = settlement_chunks_completed + 1 WHERE id = {$import_id}"; $db->exec($sql); } else if ($sheet_type === 'fund') { // 资金表分片完成,增加完成计数 $sql = "UPDATE sdb_financebase_bill_import_jdwallet SET fund_chunks_completed = fund_chunks_completed + 1 WHERE id = {$import_id}"; $db->exec($sql); } // 检查是否所有分片都已完成,如果完成则更新状态 $check_sql = "UPDATE sdb_financebase_bill_import_jdwallet SET status = 'completed' WHERE id = {$import_id} AND settlement_chunks_completed >= settlement_chunks_total AND fund_chunks_completed >= fund_chunks_total AND status != 'completed'"; $db->exec($check_sql); } else { // 处理失败,更新为失败状态 $error_msg = !empty($errmsg) ? implode('; ', $errmsg) : '数据处理失败'; $error_msg = mysql_real_escape_string($error_msg); $sql = "UPDATE sdb_financebase_bill_import_jdwallet SET status = 'failed', error_msg = '{$error_msg}' WHERE id = {$import_id}"; $db->exec($sql); } } return $result; } /** * 获取结算表标题定义(与京东日账单基本一致,但缺少结算状态字段) * @return array */ public function getTitle() { $title = array( 'order_no' => '订单编号', 'financial_no' => '单据编号', 'order_type' => '单据类型', 'goods_bn' => '商品编号', 'out_trade_no' => '商户订单号', 'goods_name' => '商品名称', 'trade_create_time' => '费用发生时间', 'trade_pay_time' => '费用计费时间', 'trade_settlement_time' => '费用结算时间', 'trade_type' => '费用项', 'amount' => '金额', 'currency' => '币种', 'bill_type' => '商家应收/应付', 'settlement_remarks' => '钱包结算备注', 'shop_bn' => '店铺号', 'jd_store_bn' => '京东门店编号', 'brand_store_bn' => '品牌门店编号', 'store_name' => '门店名称', 'remarks' => '备注', 'fee_direction' => '收支方向', 'goods_number' => '商品数量', 'jd_trade_time' => '账单日期', // 结算表使用"账单日期",不是"对账日期" ); return $title; } /** * 获取资金表标题定义 * @return array */ public function getFundTitle() { $title = array( 'merchant_no' => '商户号', 'account_code' => '账户代码', 'account_name' => '账户名称', 'date' => '日期', 'merchant_order_no' => '商户订单号', 'account_balance' => '账户余额(元)', 'income_amount' => '收入金额(元)', 'expense_amount' => '支出金额(元)', 'transaction_remark' => '交易备注', 'bill_date' => '账单日期', ); return $title; } /** * 重写getSdf方法,根据工作表类型选择处理方法 * @param array $row 行数据 * @param int $offset 行号 * @param array $title 标题 * @return array */ public function getSdf($row, $offset = 1, $title) { // 检查是否有sheet_type参数,如果有则使用对应的处理方法 $sheet_type = $this->sheet_type ?? 'settlement'; if ($sheet_type === 'fund') { // 处理资金表数据 return $this->getFundSdf($row, $offset, $title); } else { // 处理结算表数据(与京东日账单逻辑完全一致) return $this->getSettlementSdf($row, $offset, $title); } } /** * 处理资金表数据 * @param array $row 行数据 * @param int $offset 行号 * @param array $title 标题 * @return array */ public function getFundSdf($row, $offset = 1, $title) { $row = array_map('trim', $row); if (!$this->ioTitle) { $this->ioTitle = $this->getFundTitle(); $this->ioTitleKey = array_keys($this->ioTitle); } $titleKey = array(); foreach ($title as $k => $t) { $titleKey[$k] = array_search($t, $this->getFundTitle()); if ($titleKey[$k] === false) { return array('status' => false, 'msg' => '未定义字段`' . $t . '`'); } } $res = array('status' => true, 'data' => array(), 'msg' => ''); if (count($row) >= 10 && $row[0] != '商户号') { $tmp = array_combine($titleKey, $row); // 转换Excel日期字段(Excel序列号转换为日期字符串) $timeColumn = array('date', 'bill_date'); foreach ($timeColumn as $k) { if (isset($tmp[$k]) && is_numeric($tmp[$k]) && $tmp[$k] > 0) { $timestamp = ($tmp[$k] - 25569) * 86400; // Excel基准日期是1900-01-01,Unix基准是1970-01-01 $tmp[$k] = date('Y-m-d H:i:s', $timestamp); } } // 判断必填字段不能为空 foreach ($tmp as $k => $v) { if (in_array($k, array('merchant_no', 'date', 'merchant_order_no'))) { if (!$v) { $res['status'] = false; $res['msg'] = sprintf("LINE %d : %s 不能为空!", $offset, $this->ioTitle[$k]); return $res; } } } // 直接返回原始数据,父类process会调用_filterData $res['data'] = $tmp; } return $res; } /** * 处理结算表数据(与京东日账单逻辑完全一致) * @param array $row 行数据 * @param int $offset 行号 * @param array $title 标题 * @return array */ private function getSettlementSdf($row, $offset = 1, $title) { $row = array_map('trim', $row); if (!$this->ioTitle) { $this->ioTitle = $this->getTitle(); $this->ioTitleKey = array_keys($this->ioTitle); } $titleKey = array(); foreach ($title as $k => $t) { $titleKey[$k] = array_search($t, $this->getTitle()); if ($titleKey[$k] === false) { return array('status' => false, 'msg' => '未定义字段`' . $t . '`'); } } $res = array('status' => true, 'data' => array(), 'msg' => ''); if ($this->column_num <= count($row) && $row[0] != '订单编号') { $tmp = array_combine($titleKey, $row); // 转换Excel日期字段(Excel序列号转换为日期字符串) $timeColumn = array('trade_create_time', 'trade_pay_time', 'trade_settlement_time'); foreach ($timeColumn as $k) { if (isset($tmp[$k]) && is_numeric($tmp[$k]) && $tmp[$k] > 0) { $timestamp = ($tmp[$k] - 25569) * 86400; // Excel基准日期是1900-01-01,Unix基准是1970-01-01 $tmp[$k] = date('Y-m-d H:i:s', $timestamp); } } // 判断必填字段不能为空 foreach ($tmp as $k => $v) { if (in_array($k, array('order_no', 'financial_no', 'trade_settlement_time'))) { if (!$v) { $res['status'] = false; $res['msg'] = sprintf("LINE %d : %s 不能为空!", $offset, $this->ioTitle[$k]); return $res; } } } // 时间格式验证 foreach ($tmp as $k => $v) { if (in_array($k, array('trade_create_time', 'trade_pay_time', 'trade_settlement_time'))) { $result = finance_io_bill_verify::isDate($v); if ($result['status'] == 'fail') { $res['status'] = false; $res['msg'] = sprintf("LINE %d : %s 时间(%s)格式错误!", $offset, $this->ioTitle[$k], $v); return $res; } } } // 金额字段精度处理(解决浮点数精度问题) foreach ($tmp as $k => $v) { if (in_array($k, array('amount'))) { if (is_numeric($v)) { // 保留2位小数,避免浮点数精度问题 $tmp[$k] = number_format((float)$v, 2, '.', ''); } } } // 金额格式验证 foreach ($tmp as $k => $v) { if (in_array($k, array('amount'))) { $result = finance_io_bill_verify::isPrice($v); if ($result['status'] == 'fail') { $res['status'] = false; $res['msg'] = sprintf("LINE %d : %s 金额(%s)格式错误!", $offset, $this->ioTitle[$k], $v); return $res; } } } // 字段清理 foreach ($tmp as $k => $v) { if (in_array($k, array('order_no', 'financial_no', 'out_trade_no', 'goods_bn'))) { $tmp[$k] = trim($v, '=\"'); } } // 直接返回原始数据,父类process会调用_filterData $res['data'] = $tmp; } return $res; } /** * 数据过滤和转换(根据工作表类型进行不同处理) * @param array $data 原始数据 * @return array */ public function _filterData($data) { // 检查工作表类型,进行不同的数据处理 $sheet_type = $this->sheet_type ?? 'settlement'; if ($sheet_type === 'fund') { // 处理资金表数据 return $this->_filterFundData($data); } else { // 处理结算表数据(与京东日账单保持一致) return $this->_filterSettlementData($data); } } /** * 处理结算表数据(与京东日账单保持一致) * @param array $data 原始数据 * @return array */ private function _filterSettlementData($data) { $new_data = array(); $new_data['order_bn'] = $this->_getOrderBn($data); $new_data['trade_no'] = ''; $new_data['financial_no'] = $data['financial_no'];// 京东把订单号存在财务流水号里,只作为生成唯一标识用 $new_data['out_trade_no'] = $data['out_trade_no']; $new_data['trade_time'] = $data['trade_settlement_time'] ? strtotime($data['trade_settlement_time']) : 0; $new_data['trade_type'] = $data['trade_type']; $new_data['money'] = $data['amount']; $new_data['member'] = ''; $new_data['unique_id'] = $data['financial_no']; $new_data['platform_type'] = 'jdwallet'; $new_data['remarks'] = $data['remarks']; return $new_data; } /** * 获取订单号(与京东日账单保持一致) * @param array $params 参数 * @return string */ public function _getOrderBn($params) { return $params['order_no']; } /** * 资金表数据过滤和转换 * @param array $data 原始数据 * @return array */ public function _filterFundData($data) { $new_data = array(); // 从交易备注中提取订单号 $order_bn = $this->_extractOrderBnFromRemark($data['transaction_remark'] ?? ''); // 资金表数据转换为标准账单格式(与结算表保持一致) $new_data['order_bn'] = $order_bn; $new_data['trade_no'] = ''; $new_data['financial_no'] = $data['merchant_no'] ?? ''; // 使用商户号作为财务流水号 $new_data['out_trade_no'] = $data['merchant_order_no'] ?? ''; $new_data['trade_time'] = $data['date'] ? strtotime($data['date']) : 0; $new_data['trade_type'] = '资金流水'; $new_data['money'] = floatval($data['income_amount'] ?? 0) - floatval($data['expense_amount'] ?? 0); $new_data['member'] = ''; $new_data['unique_id'] = md5($data['merchant_no'] . '_' . $data['merchant_order_no'] . '_' . $data['date']); $new_data['platform_type'] = 'jdwallet_fund'; $new_data['remarks'] = $data['transaction_remark'] ?? ''; return $new_data; } /** * 从交易备注中提取订单号 * @param string $remark 交易备注 * @return string */ private function _extractOrderBnFromRemark($remark) { if (empty($remark)) { return ''; } // 匹配"订单号"后面的数字 // 支持两种模式: // 1. "售后服务单号3185685896,对应订单号327569834090" -> 提取327569834090 // 2. "价保对应订单号319578099931" -> 提取319578099931 $pattern = '/订单号(\d+)/'; if (preg_match($pattern, $remark, $matches)) { return $matches[1]; } return ''; } /** * 获取具体类别 * @param array $params 参数 * @return string */ public function getBillCategory($params) { if (!$this->rules) { $this->getRules('360buy'); // 暂时使用360buy的规则 } $this->verified_data = $params; if ($this->rules) { foreach ($this->rules as $item) { foreach ($item['rule_content'] as $rule) { if ($this->checkRule($rule)) { return $item['bill_category']; } } } } return ''; } /** * 同步到对账表 * @param array $data 原始数据 * @param string $bill_category 具体类别 * @return bool */ public function syncToBill($data, $bill_category = '') { $data['content'] = json_decode(stripslashes($data['content']), 1); if (!$data['content']) return false; $tmp = $data['content']; $shop_id = $data['shop_id']; $mdlBill = app::get('finance')->model('bill'); $oMonthlyReport = kernel::single('finance_monthly_report'); $tmp['fee_obj'] = '京东钱包'; $tmp['fee_item'] = $bill_category; $res = $this->getBillType($tmp, $shop_id); if (!$res['status']) return false; if (!$data['shop_name']) { $data['shop_name'] = isset($this->shop_list[$data['shop_id']]) ? $this->shop_list[$data['shop_id']]['name'] : ''; } // 根据工作表类型进行不同的处理 $sheet_type = $this->sheet_type ?? 'settlement'; if ($sheet_type === 'fund') { // 资金表处理 return $this->syncFundToBill($data, $tmp, $res, $mdlBill); } else { // 结算表处理 return $this->syncSettlementToBill($data, $tmp, $res, $mdlBill); } } /** * 同步结算表数据到对账表 * @param array $data 原始数据 * @param array $tmp 处理后的数据 * @param array $res 账单类型结果 * @param object $mdlBill 账单模型 * @return bool */ private function syncSettlementToBill($data, $tmp, $res, $mdlBill) { $base_sdf = array( 'order_bn' => $this->_getOrderBn($tmp), 'channel_id' => $data['shop_id'], 'channel_name' => $data['shop_name'], 'trade_time' => strtotime($tmp['trade_settlement_time']), 'fee_obj' => $tmp['fee_obj'], 'money' => round($tmp['amount'], 2), 'fee_item' => $tmp['fee_item'], 'fee_item_id' => isset($this->fee_item_rules[$tmp['fee_item']]) ? $this->fee_item_rules[$tmp['fee_item']] : 0, 'credential_number' => $tmp['financial_no'],// 单据编号 'member' => '', 'memo' => $tmp['remarks'], 'unique_id' => $data['unique_id'], 'create_time' => time(), 'fee_type' => $tmp['trade_type'], 'fee_type_id' => $res['fee_type_id'], 'bill_type' => $res['bill_type'], 'charge_status' => 1,// 流水直接设置记账成功 'charge_time' => time(), ); $base_sdf['monthly_id'] = 0; $base_sdf['monthly_item_id'] = 0; $base_sdf['monthly_status'] = 0; $base_sdf['crc32_order_bn'] = sprintf('%u', crc32($base_sdf['order_bn'])); $base_sdf['bill_bn'] = $mdlBill->gen_bill_bn(); $base_sdf['unconfirm_money'] = $base_sdf['money']; if ($mdlBill->insert($base_sdf)) { kernel::single('finance_monthly_report_items')->dealBillMatchReport($base_sdf['bill_id']); return true; } return false; } /** * 同步资金表数据到对账表 * @param array $data 原始数据 * @param array $tmp 处理后的数据 * @param array $res 账单类型结果 * @param object $mdlBill 账单模型 * @return bool */ private function syncFundToBill($data, $tmp, $res, $mdlBill) { $base_sdf = array( 'order_bn' => $this->_getOrderBn($tmp), 'channel_id' => $data['shop_id'], 'channel_name' => $data['shop_name'], 'trade_time' => strtotime($tmp['date']), // 资金表使用date字段 'fee_obj' => $tmp['fee_obj'], 'money' => round($tmp['money'], 2), // 资金表使用money字段(已计算好的净额) 'fee_item' => $tmp['fee_item'], 'fee_item_id' => isset($this->fee_item_rules[$tmp['fee_item']]) ? $this->fee_item_rules[$tmp['fee_item']] : 0, 'credential_number' => $tmp['financial_no'],// 使用商户号作为单据编号 'member' => '', 'memo' => $tmp['remarks'], 'unique_id' => $data['unique_id'], 'create_time' => time(), 'fee_type' => $tmp['trade_type'], 'fee_type_id' => $res['fee_type_id'], 'bill_type' => $res['bill_type'], 'charge_status' => 1,// 流水直接设置记账成功 'charge_time' => time(), ); $base_sdf['monthly_id'] = 0; $base_sdf['monthly_item_id'] = 0; $base_sdf['monthly_status'] = 0; $base_sdf['crc32_order_bn'] = sprintf('%u', crc32($base_sdf['order_bn'])); $base_sdf['bill_bn'] = $mdlBill->gen_bill_bn(); $base_sdf['unconfirm_money'] = $base_sdf['money']; if ($mdlBill->insert($base_sdf)) { kernel::single('finance_monthly_report_items')->dealBillMatchReport($base_sdf['bill_id']); return true; } return false; } }