Files
OMS/app/financebase/lib/data/bill/jdwallet.php
2026-01-04 19:08:31 +08:00

794 lines
29 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* Copyright 2012-2026 ShopeX (https://www.shopex.cn)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* 处理京东钱包导入
* 支持多工作表Excel文件结算表和资金表
*
* @author AI Assistant
* @version 1.0
*/
class financebase_data_bill_jdwallet extends financebase_abstract_bill
{
public $order_bn_prefix = '';
public $column_num = 22; // 结算表字段数量(比京东日账单少一个结算状态字段)
public $ioTitle = array();
public $ioTitleKey = array();
public $verified_data = array();
public $sheet_type = 'settlement'; // 工作表类型settlement或fund
/**
* 检查文件是否有效
* @param string $file_name 文件名
* @param string $file_type 文件类型
* @return array [是否有效, 错误信息, 标题]
*/
public function checkFile($file_name, $file_type)
{
try {
// 检查是否为多工作表Excel文件
if ($file_type !== 'xlsx') {
return array(false, '京东钱包导入只支持.xlsx格式的Excel文件', array());
}
// 使用PhpSpreadsheet读取多工作表
$workbook = $this->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-01Unix基准是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-01Unix基准是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-01Unix基准是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;
}
}