1. 【新增】售后单售后原因类型支持搜索

2. 【新增】手工创建订单折扣可输入正数

3. 【优化】盘点申请单确认

4. 【修复】采购退货单模拟出库失败问题

5. 【新增】订单金额客户实付与结算金额

6. 【优化】仓库发货统计报表物料名称显示

7. 【优化】自有仓储虚拟发货逻辑

8. 【修复】基础物料分类管理问题
This commit is contained in:
chenping
2026-04-01 11:59:17 +08:00
parent 9341122827
commit 61783b7d01
754 changed files with 46179 additions and 5700 deletions

View File

@@ -330,11 +330,11 @@ class financebase_data_bill_alipay extends financebase_abstract_bill
$title = array_values($this->getTitle());
sort($title);
$aliTitle = $row[1];
/* $aliTitle = $row[1];
sort($aliTitle);
if ($title == $aliTitle) {
return array(true, '文件模板匹配', $row[1]);
}
} */
$aliTitle = $row[4];
sort($aliTitle);

View File

@@ -0,0 +1,251 @@
<?php
/**
* 处理国补金额导入
*
* @author AI Assistant
* @version 1.0
*/
class financebase_data_bill_guobu extends financebase_abstract_bill
{
public $column_num = 5; // 5个字段订单号、国补金额、SKU、数量、账单日期
public $ioTitle = array();
public $ioTitleKey = array();
private static $shop_type_cache = array(); // 静态变量缓存店铺类型数据
public function getTitle()
{
$title = array(
'order_bn' => '订单号',
'amount' => '国补金额',
'sku' => 'SKU',
'quantity' => '数量',
'trade_time' => '账单日期'
);
return $title;
}
public function getSdf($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);
// 必填字段验证
$required_fields = array('order_bn', 'amount', 'sku', 'quantity', 'trade_time');
foreach ($required_fields as $field) {
if (empty($tmp[$field])) {
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 不能为空!", $offset, $this->ioTitle[$field]);
return $res;
}
}
// 金额格式验证
if (!is_numeric($tmp['amount'])) {
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 金额(%s)格式错误!", $offset, $this->ioTitle['amount'], $tmp['amount']);
return $res;
}
// 数量格式验证
if (!is_numeric($tmp['quantity']) || $tmp['quantity'] <= 0) {
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 数量(%s)必须为正数!", $offset, $this->ioTitle['quantity'], $tmp['quantity']);
return $res;
}
// 日期格式验证
if (!strtotime($tmp['trade_time'])) {
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 日期(%s)格式错误!", $offset, $this->ioTitle['trade_time'], $tmp['trade_time']);
return $res;
}
$res['data'] = $tmp;
}
return $res;
}
public function _filterData($data)
{
$new_data = array();
$new_data['order_bn'] = $this->_getOrderBn($data);
$new_data['trade_no'] = '';
$new_data['financial_no'] = $data['order_bn'];
$new_data['out_trade_no'] = '';
$new_data['trade_time'] = strtotime($data['trade_time']);
$new_data['trade_type'] = '国补金额';
$new_data['money'] = $data['amount'];
$new_data['member'] = '';
// unique_id 生成规则shop_id + 订单号 + SKU + 国补金额的MD5
$shop_id = $data['shop_id'] ?: $this->getCurrentShopId();
$new_data['unique_id'] = md5($shop_id . '-' . $data['order_bn'] . '-' . $data['sku'] . '-' . $data['amount']);
$shop_info = $this->getShopInfoByShopId($shop_id);
$new_data['platform_type'] = $shop_info ? $shop_info['shop_type'] : '';
$new_data['remarks'] = '国补金额导入-SKU:' . $data['sku'] . ' 数量:' . $data['quantity'];
return $new_data;
}
public function _getOrderBn($params)
{
return $params['order_bn'];
}
public function getBillCategory($params)
{
// 国补金额固定类别
return '国补金额';
}
/**
* 检查文件是否有效
* @param string $file_name 文件名
* @param string $file_type 文件类型
* @return array [是否有效, 错误信息, 标题]
*/
public function checkFile($file_name, $file_type)
{
$ioType = kernel::single('financebase_io_' . $file_type);
$row = $ioType->getData($file_name, 0, 1);
$title = array_values($this->getTitle());
sort($title);
$guobuTitle = $row[0];
sort($guobuTitle);
if ($title == $guobuTitle) {
return array(true, '文件模板匹配', $row[0]);
}
if (!array_diff($guobuTitle, $title)) {
return array(true, '文件模板匹配', $row[0]);
}
return array(false, '文件模板错误:' . var_export($row[0], true) . ',正确的为:' . var_export($title, 1));
}
/**
* 获取导入日期列配置
* @param array $title 标题数组
* @return array
*/
public function getImportDateColunm($title = null)
{
// 国补导入的日期字段配置
$timeColumn = ['账单日期'];
$timeCol = array();
foreach ($timeColumn as $v) {
if($k = array_search($v, $title)) {
$timeCol[] = $k+1;
}
}
$timezone = defined('DEFAULT_TIMEZONE') ? DEFAULT_TIMEZONE : 0;
return array('column' => $timeCol, 'time_diff' => $timezone * 3600);
}
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'] = '国补金额';
$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'] : '';
}
$base_sdf = array(
'order_bn' => $tmp['order_bn'],
'channel_id' => $data['shop_id'],
'channel_name' => $data['shop_name'],
'trade_time' => strtotime($tmp['trade_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['order_bn'],
'member' => '',
'memo' => $tmp['remarks'],
'unique_id' => $data['unique_id'],
'create_time' => time(),
'fee_type' => $tmp['fee_item'],
'fee_type_id' => $res['fee_type_id'],
'bill_type' => $res['bill_type'],
'charge_status' => 1,
'charge_time' => time(),
'monthly_id' => 0,
'monthly_item_id' => 0,
'monthly_status' => 0,
'crc32_order_bn' => sprintf('%u', crc32($tmp['order_bn'])),
'bill_bn' => $mdlBill->gen_bill_bn(),
'unconfirm_money' => round($tmp['money'], 2),
);
if ($mdlBill->insert($base_sdf)) {
kernel::single('finance_monthly_report_items')->dealBillMatchReport($base_sdf['bill_id']);
return true;
}
return false;
}
private function getShopInfoByShopId($shop_id)
{
if (!$shop_id) {
return null;
}
// 如果缓存为空,一次性查询所有店铺数据
if (empty(self::$shop_type_cache)) {
$mdlShop = app::get('ome')->model('shop');
$shop_list = $mdlShop->getList('shop_id,shop_type');
// 将数据存入静态缓存key为shop_idvalue为shop数组
foreach ($shop_list as $shop) {
self::$shop_type_cache[$shop['shop_id']] = $shop;
}
}
// 从缓存中获取shop信息
if (isset(self::$shop_type_cache[$shop_id])) {
return self::$shop_type_cache[$shop_id];
}
return null;
}
private function getCurrentShopId()
{
// 从当前请求中获取shop_id
return $_POST['shop_id'] ?? '';
}
}

View File

@@ -0,0 +1,352 @@
<?php
/**
* 处理京东E卡结算单导入
*
* @author 334395174@qq.com
* @version 0.1
*/
class financebase_data_bill_jdecard extends financebase_abstract_bill
{
public $order_bn_prefix = '';
public $column_num = 11;
public $ioTitle = array();
public $ioTitleKey = array();
public $verified_data = array();
public $shop_list_by_name = array();
// 处理数据
public function getSdf($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) and $row[0] != '订单编号')
{
$tmp = array_combine($titleKey, $row);
// 判断必填参数不能为空
foreach ($tmp as $k => $v) {
// 必填字段验证:订单编号、费用项、金额
if(in_array($k, array('order_no', 'trade_type', 'amount')))
{
if(!$v)
{
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 不能为空!", $offset, $this->ioTitle[$k]);
return $res;
}
}
// 时间格式验证
if(in_array($k, array('order_create_time', 'order_complete_time')))
{
if($v) {
$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;
}
}
}
// 金额格式验证
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;
}
}
// 清理特殊字符(订单编号、商品编号等)
if(in_array($k, array('order_no', 'goods_bn', 'branch_id', 'store_bn'))){
$tmp[$k] = trim($v, '=\"');
}
}
$res['data'] = $tmp;
}
return $res;
}
public function getTitle()
{
$title = array(
'order_no' => '订单编号',
'goods_bn' => '商品编号',
'goods_number' => 'SKU数量',
'goods_name' => '商品名称',
'order_create_time' => '下单时间',
'order_complete_time' => '完成时间',
'trade_type' => '费用项',
'amount' => '金额',
'branch_id' => '分公司id',
'store_bn' => '门店编号',
'tax_rate' => '税率',
);
return $title;
}
// 获取订单号
public function _getOrderBn($params)
{
// 直接返回订单编号
if (isset($params['order_no']) && $params['order_no']) {
return $params['order_no'];
}
return '';
}
/**
* 获取具体类别
* @Author YangYiChao
* @Date 2019-06-03
* @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 '';
}
/**
* 检查文件是否有效
* @Author YangYiChao
* @Date 2019-06-25
* @param String $file_name 文件名
* @param String $file_type 文件类型
* @return Boolean
*/
public function checkFile($file_name, $file_type)
{
if ($file_type !== 'xlsx') {
return array(false, '京东E卡导入只支持.xlsx格式的Excel文件');
}
$ioType = kernel::single('financebase_io_' . $file_type);
$row = $ioType->getData($file_name, 0, 1, 0); // 读取第一个工作表
if (empty($row) || !isset($row[0])) {
return array(false, '文件没有数据');
}
$title = array_values($this->getTitle());
sort($title);
$jdecardTitle = $row[0];
sort($jdecardTitle);
// 完全匹配
if ($title == $jdecardTitle) {
return array(true, '文件模板匹配', $row[0]);
}
// 包含所有必需列
if (!array_diff($jdecardTitle, $title)) {
return array(true, '文件模板匹配', $row[0]);
}
return array(false, '文件模板错误:' . var_export($row[0], true) . ',正确的为:' . var_export($title, 1));
}
/**
* 同步到对账表
* @Author YangYiChao
* @Date 2019-06-25
* @param Array 原始数据 $data
* @param String 具体类别 $bill_category
* @return Boolean
*/
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'] = '京东E卡';
$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'] : '';
}
$base_sdf = array(
'order_bn' => $this->_getOrderBn($tmp),
'channel_id' => $data['shop_id'],
'channel_name' => $data['shop_name'],
'trade_time' => isset($tmp['order_complete_time']) ? strtotime($tmp['order_complete_time']) : 0,
'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' => isset($tmp['financial_no']) ? $tmp['financial_no'] : '', // 使用生成的唯一编码
'member' => '',
'memo' => isset($tmp['goods_name']) ? $tmp['goods_name'] : '', // 备注保存商品名称
'unique_id' => $data['unique_id'],
'create_time' => time(),
'fee_type' => isset($tmp['trade_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;
}
// 更新订单号
public function updateOrderBn($data)
{
$this->_formatData($data);
$mdlBill = app::get('finance')->model('bill');
if(!$this->shop_list_by_name)
{
$this->shop_list_by_name = financebase_func::getShopList(financebase_func::getShopType());
$this->shop_list_by_name = array_column($this->shop_list_by_name, null, 'name');
}
foreach ($data as $v)
{
if('订单编号' == $v[0]) continue;
// 假设最后3列是店铺名称、账单号、订单号
if(!isset($v[11]) || !isset($v[12]) || !isset($v[13])) continue;
if(!$v[11] || !$v[12] || !$v[13]) continue;
$shop_id = isset($this->shop_list_by_name[$v[11]]) ? $this->shop_list_by_name[$v[11]]['shop_id'] : 0;
if(!$shop_id) continue;
$filter = array('bill_bn' => $v[12], 'shop_id' => $shop_id);
// 找到unique_id
$bill_info = $mdlBill->getList('unique_id,bill_id', $filter, 0, 1);
if(!$bill_info) continue;
$bill_info = $bill_info[0];
if($mdlBill->update(array('order_bn' => $v[13]), array('bill_id' => $bill_info['bill_id'])))
{
app::get('financebase')->model('bill')->update(array('order_bn' => $v[13]), array('unique_id' => $bill_info['unique_id'], 'shop_id' => $shop_id));
$op_name = kernel::single('desktop_user')->get_name();
$content = sprintf("订单号改成:%s", $v[13]);
finance_func::addOpLog($v[12], $op_name, $content, '更新订单号');
}
}
}
public function _filterData($data)
{
$new_data = array();
$new_data['order_bn'] = $this->_getOrderBn($data);
$new_data['trade_no'] = isset($data['order_no']) ? $data['order_no'] : ''; // 使用订单编号
// 生成唯一编码order_no + goods_bn + goods_number + trade_type + amount 的 MD5
// 必须加入金额,因为同一订单同一商品可能既有正金额(货款)又有负金额(价保扣款)
$unique_str = sprintf(
'%s_%s_%s_%s_%s',
isset($data['order_no']) ? $data['order_no'] : '',
isset($data['goods_bn']) ? $data['goods_bn'] : '',
isset($data['goods_number']) ? $data['goods_number'] : '',
isset($data['trade_type']) ? $data['trade_type'] : '',
isset($data['amount']) ? $data['amount'] : ''
);
$unique_code = md5($unique_str);
$new_data['financial_no'] = $unique_code; // 使用生成的唯一编码
$new_data['out_trade_no'] = isset($data['goods_bn']) ? $data['goods_bn'] : ''; // 使用商品编号
$new_data['trade_time'] = isset($data['order_complete_time']) ? strtotime($data['order_complete_time']) : 0;
$new_data['trade_type'] = isset($data['trade_type']) ? $data['trade_type'] : '';
$new_data['money'] = isset($data['amount']) ? floatval($data['amount']) : 0; // 保留正负
$new_data['member'] = '';
// 使用生成的唯一编码作为唯一标识
$new_data['unique_id'] = $unique_code;
$new_data['platform_type'] = 'jdecard';
$new_data['remarks'] = ''; // 备注为空
return $new_data;
}
public function getImportDateColunm($title=null)
{
$timeColumn = array('下单时间', '完成时间');
$timeCol = array();
if ($title) {
foreach ($timeColumn as $v) {
$k = array_search($v, $title);
if ($k !== false) {
$timeCol[] = $k + 1;
}
}
}
$timezone = defined('DEFAULT_TIMEZONE') ? DEFAULT_TIMEZONE : 0;
return array('column' => $timeCol, 'time_diff' => $timezone * 3600);
}
}

View File

@@ -0,0 +1,427 @@
<?php
/**
* 处理京东国补结算单导入
* 多个金额列转换为多行数据(类似抖音处理逻辑)
*
* @author 334395174@qq.com
* @version 0.1
*/
class financebase_data_bill_jdguobu extends financebase_abstract_bill
{
public $order_bn_prefix = '';
public $column_num = 19; // 19个字段
public $ioTitle = array();
public $ioTitleKey = array();
public $verified_data = array();
public $shop_list_by_name = array();
// 需要转换为行的金额列金额不为0时才处理
public $column_to_row = [
'commission', // 佣金
'sop_amount', // SOP采购货款
'service_fee', // 交易服务费
'guobu_amount', // 国补采购款
'large_store_amount', // 大店业务采购货款
'promotion_service_fee', // 促销服务费
'ad_commission', // 广告联合活动降扣佣金
'link_service_fee', // 超链3.0服务费
'final_amount', // 应结货款
];
/**
* 获取标题映射
* @return array
*/
public function getTitle()
{
$title = array(
'billing_time' => '计费时间',
'complete_time' => '完成时间',
'order_no' => '订单编号',
'order_total_amount' => '订单总金额',
'merchant_promotion' => '商家促销金额',
'full_reduction' => '满减优惠金额',
'shop_jingquan' => '店铺京券金额',
'shop_dongquan' => '店铺东券金额',
'payment' => '货款',
'commission' => '佣金',
'sop_amount' => 'SOP采购货款',
'service_fee' => '交易服务费',
'guobu_amount' => '国补采购款',
'large_store_amount' => '大店业务采购货款',
'promotion_service_fee' => '促销服务费',
'ad_commission' => '广告联合活动降扣佣金',
'link_service_fee' => '超链3.0服务费',
'final_amount' => '应结货款',
'remarks' => '备注',
);
return $title;
}
/**
* 批量转换数据(列转行)
* @param array $data 原始数据
* @param array $title 标题行
* @return array
*/
public function batchTransferData($data, $title)
{
if (!$this->ioTitle) {
$this->ioTitle = $this->getTitle();
}
$titleKey = array();
foreach ($title as $k => $t) {
$titleKey[$k] = array_search($t, $this->getTitle());
}
$reData = [];
foreach($data as $row) {
$row = array_map('trim', $row);
// 跳过标题行
if($row[0] == '计费时间') {
continue;
}
// 过滤"合计"行:订单编号为空 或 包含"合计"字样
$order_no_index = array_search('order_no', $titleKey);
if($order_no_index !== false) {
$order_no_value = isset($row[$order_no_index]) ? trim($row[$order_no_index]) : '';
// 如果订单号为空,或者整行数据中包含"合计",则跳过
if(empty($order_no_value) || $this->containsTotal($row)) {
continue;
}
}
$tmpRow = [];
foreach($row as $k => $v) {
if(isset($titleKey[$k]) && $titleKey[$k]) {
$tmpRow[$titleKey[$k]] = $v;
}
}
// 将每个金额列转换为单独的一行数据忽略金额为0的数据
foreach($this->column_to_row as $col) {
// 检查金额是否存在、不为空、不为0包括 '0'、'0.00'、0、0.0 等)
if(isset($tmpRow[$col]) && $tmpRow[$col] !== '' && floatval($tmpRow[$col]) != 0) {
$tmp = $tmpRow;
$tmp['amount'] = $tmpRow[$col];
$tmp['trade_type'] = $this->ioTitle[$col];
$reData[] = $tmp;
}
}
}
return $reData;
}
/**
* 检查行数据是否包含"合计"
* @param array $row
* @return bool
*/
private function containsTotal($row)
{
foreach($row as $cell) {
if(strpos($cell, '合计') !== false) {
return true;
}
}
return false;
}
/**
* 处理数据(核心方法)
* @param array $row 数据行
* @param int $offset 行偏移量
* @param array $title 标题行
* @return array
*/
public function getSdf($row, $offset=1, $title)
{
$res = array('status'=>true, 'data'=>array(), 'msg'=>'');
$tmp = [];
// 判断参数不能为空
foreach ($row as $k => $v) {
$tmp[$k] = trim($v, '\'');
// 必填字段验证:订单编号、费用类型
if(in_array($k, array('order_no', 'trade_type')))
{
if(!$v)
{
$res['status'] = false;
$res['msg'] = sprintf("%s : %s 不能为空!", isset($row['order_no']) ? $row['order_no'] : 'LINE '.$offset, $this->ioTitle[$k]);
return $res;
}
}
// 时间格式验证
if(in_array($k, array('billing_time', 'complete_time')))
{
if($v) {
$result = finance_io_bill_verify::isDate($v);
if ($result['status'] == 'fail')
{
$res['status'] = false;
$res['msg'] = sprintf("%s : %s 时间(%s)格式错误!", isset($row['order_no']) ? $row['order_no'] : 'LINE '.$offset, $this->ioTitle[$k], $v);
return $res;
}
}
}
// 金额格式验证
if(in_array($k, array('amount')))
{
if($v) {
$result = finance_io_bill_verify::isPrice($v);
if ($result['status'] == 'fail')
{
$res['status'] = false;
$res['msg'] = sprintf("%s : %s 金额(%s)格式错误!", isset($row['order_no']) ? $row['order_no'] : 'LINE '.$offset, $this->ioTitle[$k], $v);
return $res;
}
}
}
}
$res['data'] = $tmp;
return $res;
}
/**
* 获取订单编号
* @param array $params
* @return string
*/
public function _getOrderBn($params)
{
if (isset($params['order_no']) && $params['order_no']) {
return $params['order_no'];
}
return '';
}
/**
* 过滤数据
* @param array $data
* @return array
*/
public function _filterData($data)
{
$new_data = array();
$new_data['order_bn'] = $this->_getOrderBn($data);
$new_data['trade_no'] = isset($data['order_no']) ? $data['order_no'] : '';
$new_data['financial_no'] = isset($data['order_no']) ? $data['order_no'] : '';
$new_data['out_trade_no'] = '';
$new_data['trade_time'] = isset($data['billing_time']) ? strtotime($data['billing_time']) : 0;
$new_data['trade_type'] = isset($data['trade_type']) ? $data['trade_type'] : '';
$new_data['money'] = isset($data['amount']) ? floatval($data['amount']) : 0; // 保留正负
$new_data['member'] = '';
// 使用订单号+费用类型组合作为唯一标识
$unique_str = sprintf('%s-%s',
isset($data['order_no']) ? $data['order_no'] : '',
isset($data['trade_type']) ? $data['trade_type'] : ''
);
$new_data['unique_id'] = md5($unique_str);
$new_data['platform_type'] = 'jdguobu';
$new_data['remarks'] = isset($data['remarks']) ? $data['remarks'] : '';
return $new_data;
}
/**
* 获取账单分类
* @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 string $file_name 文件名
* @param string $file_type 文件类型
* @return array
*/
public function checkFile($file_name, $file_type)
{
if ($file_type !== 'xlsx') {
return array(false, '京东国补导入只支持.xlsx格式的Excel文件');
}
$ioType = kernel::single('financebase_io_' . $file_type);
$row = $ioType->getData($file_name, 0, 5, 0);
if (empty($row) || !isset($row[0])) {
return array(false, '文件没有数据');
}
$title = array_values($this->getTitle());
$plateTitle = $row[0];
// 检查是否包含所有必需的列
foreach($title as $v) {
if(array_search($v, $plateTitle) === false) {
return array(false, '文件模板错误:列【'.$v.'】未包含在导入文件中');
}
}
return array(true, '文件模板匹配', $row[0]);
}
/**
* 同步到对账表finance.bill
* @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'] : '';
}
$base_sdf = array(
'order_bn' => $this->_getOrderBn($tmp),
'channel_id' => $data['shop_id'],
'channel_name' => $data['shop_name'],
'trade_time' => isset($tmp['billing_time']) ? strtotime($tmp['billing_time']) : 0,
'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' => isset($tmp['financial_no']) ? $tmp['financial_no'] : '',
'member' => '',
'memo' => isset($tmp['remarks']) ? $tmp['remarks'] : '',
'unique_id' => $data['unique_id'],
'create_time' => time(),
'fee_type' => isset($tmp['trade_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 数据
* @return void
*/
public function updateOrderBn($data)
{
$this->_formatData($data);
$mdlBill = app::get('finance')->model('bill');
if(!$this->shop_list_by_name)
{
$this->shop_list_by_name = financebase_func::getShopList(financebase_func::getShopType());
$this->shop_list_by_name = array_column($this->shop_list_by_name, null, 'name');
}
foreach ($data as $v)
{
if('计费时间' == $v[0]) continue;
// 需要有店铺名称、账单号、订单号才能更新
if(!isset($v[19]) || !isset($v[20]) || !isset($v[21])) continue;
if(!$v[19] || !$v[20] || !$v[21]) continue;
$shop_id = isset($this->shop_list_by_name[$v[19]]) ? $this->shop_list_by_name[$v[19]]['shop_id'] : 0;
if(!$shop_id) continue;
$filter = array('bill_bn' => $v[20], 'shop_id' => $shop_id);
// 找到unique_id
$bill_info = $mdlBill->getList('unique_id,bill_id', $filter, 0, 1);
if(!$bill_info) continue;
$bill_info = $bill_info[0];
if($mdlBill->update(array('order_bn' => $v[21]), array('bill_id' => $bill_info['bill_id'])))
{
app::get('financebase')->model('bill')->update(array('order_bn' => $v[21]), array('unique_id' => $bill_info['unique_id'], 'shop_id' => $shop_id));
$op_name = kernel::single('desktop_user')->get_name();
$content = sprintf("订单号改成:%s", $v[21]);
finance_func::addOpLog($v[20], $op_name, $content, '更新订单号');
}
}
}
/**
* 获取导入日期列
* @param array|null $title
* @return array
*/
public function getImportDateColunm($title=null)
{
$timeColumn = array('计费时间', '完成时间');
$timeCol = array();
if ($title) {
foreach ($timeColumn as $v) {
$k = array_search($v, $title);
if ($k !== false) {
$timeCol[] = $k + 1;
}
}
}
$timezone = defined('DEFAULT_TIMEZONE') ? DEFAULT_TIMEZONE : 0;
return array('column' => $timeCol, 'time_diff' => $timezone * 3600);
}
}

View File

@@ -69,7 +69,7 @@ class financebase_data_bill_jdwallet extends financebase_abstract_bill
$fund_sheet = $workbook['fund'];
$fund_headers = $this->getSheetHeaders($fund_sheet);
$required_fund_fields = array('商户号', '日期', '商户订单号');
$required_fund_fields = array('创建时间', '商户订单号');
foreach ($required_fund_fields as $field) {
if (!in_array($field, $fund_headers)) {
return array(false, '资金表缺少必填字段:' . $field, array());
@@ -157,7 +157,7 @@ class financebase_data_bill_jdwallet extends financebase_abstract_bill
if ($sheet_name === '结算表') {
$timeColumn = array('费用结算时间', '费用计费时间', '费用发生时间');
} else if ($sheet_name === '资金表') {
$timeColumn = array('日期', '账单日期');
$timeColumn = array('创建时间');
} else {
$timeColumn = array();
}
@@ -333,16 +333,18 @@ class financebase_data_bill_jdwallet extends financebase_abstract_bill
public function getFundTitle()
{
$title = array(
'merchant_no' => '商户号',
'create_time' => '创建时间',
'account_code' => '账户代码',
'account_name' => '账户名称',
'date' => '日期',
'merchant_order_no' => '商户订单号',
'account_balance' => '账户余额(元)',
'currency' => '币种',
'income_amount' => '收入金额(元)',
'expense_amount' => '支出金额(元)',
'transaction_remark' => '交易备注',
'bill_date' => '账单日期',
'account_balance' => '账户余额(元)',
'trade_type' => '交易类型',
'out_trade_no' => '商户订单号',
'trade_order_no' => '交易订单号',
'original_merchant_order_no' => '原商户订单号',
'remarks' => '资金动账备注',
);
return $title;
@@ -396,11 +398,11 @@ class financebase_data_bill_jdwallet extends financebase_abstract_bill
$res = array('status' => true, 'data' => array(), 'msg' => '');
if (count($row) >= 10 && $row[0] != '商户号') {
if (count($row) >= 12 && $row[0] != '创建时间') {
$tmp = array_combine($titleKey, $row);
// 转换Excel日期字段Excel序列号转换为日期字符串
$timeColumn = array('date', 'bill_date');
$timeColumn = array('create_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
@@ -410,7 +412,7 @@ class financebase_data_bill_jdwallet extends financebase_abstract_bill
// 判断必填字段不能为空
foreach ($tmp as $k => $v) {
if (in_array($k, array('merchant_no', 'date', 'merchant_order_no'))) {
if (in_array($k, array('create_time', 'out_trade_no'))) {
if (!$v) {
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 不能为空!", $offset, $this->ioTitle[$k]);
@@ -419,6 +421,12 @@ class financebase_data_bill_jdwallet extends financebase_abstract_bill
}
}
// 计算money字段收入金额 - 支出金额)
$tmp['money'] = floatval($tmp['income_amount'] ?? 0) - floatval($tmp['expense_amount'] ?? 0);
// 设置financial_no字段
$tmp['financial_no'] = $tmp['trade_order_no'] ?? '';
// 直接返回原始数据父类process会调用_filterData
$res['data'] = $tmp;
}
@@ -591,22 +599,23 @@ class financebase_data_bill_jdwallet extends financebase_abstract_bill
{
$new_data = array();
// 从交易备注中提取订单号
$order_bn = $this->_extractOrderBnFromRemark($data['transaction_remark'] ?? '');
// 从资金动账备注中提取订单号
$order_bn = $this->_extractOrderBnFromRemark($data['remarks'] ?? '');
// 资金表数据转换为标准账单格式(与结算表保持一致)
$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['financial_no'] = $data['financial_no'] ?? ''; // 使用财务流水号
$new_data['out_trade_no'] = $data['out_trade_no'] ?? '';
$new_data['trade_time'] = $data['create_time'] ? strtotime($data['create_time']) : 0;
$new_data['trade_type'] = !empty($data['trade_type']) ? $data['trade_type'] : '资金流水';
$new_data['money'] = $data['money'] ?? 0;
$new_data['member'] = '';
$new_data['unique_id'] = md5($data['merchant_no'] . '_' . $data['merchant_order_no'] . '_' . $data['date']);
// $new_data['unique_id'] = md5($data['account_code'] . '_' . $data['out_trade_no'] . '_' . $data['create_time']);
$new_data['unique_id'] = $data['financial_no'] ?? '';
$new_data['platform_type'] = 'jdwallet_fund';
$new_data['remarks'] = $data['transaction_remark'] ?? '';
$new_data['remarks'] = $data['remarks'] ?? '';
return $new_data;
}
@@ -759,12 +768,12 @@ class financebase_data_bill_jdwallet extends financebase_abstract_bill
'order_bn' => $this->_getOrderBn($tmp),
'channel_id' => $data['shop_id'],
'channel_name' => $data['shop_name'],
'trade_time' => strtotime($tmp['date']), // 资金表使用date字段
'trade_time' => strtotime($tmp['create_time']), // 资金表使用create_time字段
'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'],// 使用商户号作为单据编号
'credential_number' => $tmp['financial_no'],// 使用账户代码作为单据编号
'member' => '',
'memo' => $tmp['remarks'],
'unique_id' => $data['unique_id'],
@@ -790,4 +799,59 @@ class financebase_data_bill_jdwallet extends financebase_abstract_bill
return false;
}
// 更新订单号
public function updateOrderBn($data)
{
$this->_formatData($data);
$mdlBill = app::get('finance')->model('bill');
if(!$this->shop_list_by_name)
{
$this->shop_list_by_name = financebase_func::getShopList(financebase_func::getShopType());
$this->shop_list_by_name = array_column($this->shop_list_by_name,null,'name');
}
foreach ($data as $v)
{
if('订单编号' == $v[0]) continue;
if(!$v[21] || !$v[22] || !$v[23]) continue;
$shop_id = isset($this->shop_list_by_name[$v[21]]) ? $this->shop_list_by_name[$v[21]]['shop_id'] : 0;
if(!$shop_id) continue;
$filter = array('bill_bn'=>$v[22],'shop_id'=>$shop_id);
// 找到unique_id
$bill_info = $mdlBill->getList('unique_id,bill_id',$filter,0,1);
if(!$bill_info) continue;
$bill_info = $bill_info[0];
if($mdlBill->update(array('order_bn'=>$v[23]),array('bill_id'=>$bill_info['bill_id'])))
{
app::get('financebase')->model('bill')->update(array('order_bn'=>$v[23]),array('unique_id'=>$bill_info['unique_id'],'shop_id'=>$shop_id));
$op_name = kernel::single('desktop_user')->get_name();
$content = sprintf("订单号改成:%s",$v[23]);
finance_func::addOpLog($v[22],$op_name,$content,'更新订单号');
}
}
}
public function getImportDateColunm($title=null)
{
$timeColumn = ['费用结算时间','费用计费时间','费用发生时间'];
$timeCol = [];
foreach ($timeColumn as $v) {
if($k = array_search($v, $title)) {
$timeCol[] = $k+1;
}
}
$timezone = defined('DEFAULT_TIMEZONE') ? DEFAULT_TIMEZONE : 0;
return array('column'=>$timeCol,'time_diff'=>$timezone * 3600 );
}
}

View File

@@ -0,0 +1,21 @@
<?php
/**
* 处理京东钱包导入
* 支持多工作表Excel文件结算表和资金表
*
* @author AI Assistant
* @version 1.0
*/
class financebase_data_bill_jdwallet_fund extends financebase_data_bill_jdwallet
{
/**
* 获取资金表标题定义(与京东日账单基本一致,但缺少结算状态字段)
* @return array
*/
public function getTitle()
{
return parent::getFundTitle();
}
}

View File

@@ -0,0 +1,322 @@
<?php
/**
* 处理喵速达账单导入
*
* @author AI Assistant
* @version 1.0
*/
class financebase_data_bill_miaosuda extends financebase_abstract_bill
{
public $order_bn_prefix = '';
public $column_num = 34; // 喵速达账单有34列
public $ioTitle = array();
public $ioTitleKey = array();
public $verified_data = array();
/**
* 处理数据
*/
public function getSdf($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);
// 验证必填字段
foreach ($tmp as $k => $v) {
// 必填字段验证(核心字段)
if (in_array($k, array('bill_code', 'main_order_no', 'business_time', 'amount'))) {
if (!$v && $v !== '0' && $v !== 0) {
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 不能为空!", $offset, $this->ioTitle[$k]);
return $res;
}
}
// 时间格式验证
if (in_array($k, array('bill_create_time', 'business_time', 'trade_pay_time'))) {
if ($v && trim($v) !== '') {
$result = finance_io_bill_verify::isDate($v);
if ($result['status'] == 'fail' || strtotime($v) <= 0) {
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 时间格式错误!取值是:%s", $offset, $this->ioTitle[$k], $v);
return $res;
}
}
}
// 金额格式验证喵速达支持多位小数如未税金额可能有6位小数
if (in_array($k, array('amount', 'amount_without_tax', 'tax_amount', 'unit_price_with_tax'))) {
if ($v && trim($v) !== '') {
// 自定义金额验证:支持多位小数的正负数
if (!preg_match('/^-?\d+(\.\d+)?$/', $v)) {
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 金额格式错误!取值是:%s", $offset, $this->ioTitle[$k], $v);
return $res;
}
}
}
// 处理订单号去除Excel的="xxx"格式)
if (in_array($k, array('bill_code', 'main_order_no', 'sub_order_no', 'purchase_order_no', 'financial_no', 'order_no'))) {
$tmp[$k] = trim($v, '=\"');
}
}
$res['data'] = $tmp;
}
return $res;
}
/**
* 获取字段定义
*
* 根据喵速达Excel模板的实际列名定义共34列
*/
public function getTitle()
{
$title = array(
'bill_code' => '对账单编码', // S2025102032246311035
'bill_create_time' => '账单创建时间', // 2025-10-20 14:00:17
'main_order_no' => '业务主单据编码', // PO036125100821303611874066166
'sub_order_no' => '业务子单据编码', // IO25100821303611874066166252507
'business_time' => '业务时间', // 2025-10-08 21:30:36
'source_doc_type' => '来源单据类型', // 采购入库
'fee_code' => '费用项编码', // HM_PAYMENT_GOODS
'trade_type' => '结算方式', // 货款
'supplier_code' => '供应商编码', // 488675267
'supplier_name' => '供应商名称', // 美诺电器有限公司冰洗厨热商家仓配
'currency' => '结算币种', // CNY
'amount' => '含税金额', // 11770.56 (主要金额字段)
'amount_without_tax' => '未税金额', // 10416.424779
'tax_amount' => '税额', // 1354.135221
'tax_rate' => '税率', // 0.13
'goods_code' => '货品编码', // 898392380713
'goods_name' => '货品名称', // TCH797WP C 莲花白
'billing_quantity' => '计费数量', // 1.000000
'unit_price_with_tax' => '含税单价', // 11770.560000
'is_recalculate' => '是否重算', // 计算数据
'financial_no' => '唯一编码', // KLZYS202510086465904920660
'purchase_order_no' => '采购单编码', // PO036125100821303611874066166
'settlement_mode' => '结算模式', // PURCHASE
'industry_name' => '行业名称', // 数码家电
'order_no' => '二段LP单号', // LP00738139484471
'trade_sub_order_no' => '交易子单编码', // 2591505374260069772
'trade_main_order_no' => '交易主单编码', // 2591505374260069772
'refund_quantity' => '原子货品退款数量', // 空
'refund_amount' => '原子货品消费者退款金额', // 空
'goods_unit' => '货品单位', // 空
'trade_pay_time' => '交易支付时间', // 空
'refund_order_no' => '退款单号', // 空
'sales_quantity' => '销售数量(退款数量)', // 1.000000
'billing_formula' => '计费公式', // 长文本
);
return $title;
}
/**
* 获取订单号
*/
public function _getOrderBn($params)
{
// 直接使用二段LP单号
if (isset($params['order_no']) && $params['order_no']) {
return $params['order_no'];
}
return '';
}
/**
* 获取具体类别
*/
public function getBillCategory($params)
{
if (!$this->rules) {
$this->getRules('miaosuda');
}
$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 '';
}
/**
* 检查文件是否有效
*/
public function checkFile($file_name, $file_type)
{
$ioType = kernel::single('financebase_io_' . $file_type);
$row = $ioType->getData($file_name, 0, 3);
// 检查第一行是否包含"对账单编码"
if (!isset($row[0][0]) || $row[0][0] != '对账单编码') {
return array(false, '文件模板错误:第一列应该是"对账单编码"');
}
$title = array_values($this->getTitle());
sort($title);
$fileTitle = $row[0];
sort($fileTitle);
if (!array_diff($fileTitle, $title)) {
return array(true, '文件模板匹配', $row[0]);
}
return array(false, '文件模板错误:列名不匹配');
}
/**
* 同步到对账表
*/
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');
$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'] : '';
}
// 使用含税金额作为主要金额(保留正负,负数表示支出/退款)
$amount = isset($tmp['amount']) ? floatval($tmp['amount']) : 0;
$base_sdf = array(
'order_bn' => $this->_getOrderBn($tmp),
'channel_id' => $data['shop_id'],
'channel_name' => $data['shop_name'],
'trade_time' => strtotime($tmp['business_time']), // 使用业务时间
'fee_obj' => $tmp['fee_obj'],
'money' => round($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' => isset($tmp['financial_no']) ? $tmp['financial_no'] : '', // 使用唯一编码(财务流水号)
'member' => isset($tmp['supplier_name']) ? $tmp['supplier_name'] : '',
'memo' => isset($tmp['source_doc_type']) ? $tmp['source_doc_type'] : '', // 来源单据类型
'unique_id' => $data['unique_id'],
'create_time' => time(),
'fee_type' => isset($tmp['trade_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;
}
/**
* 提取关键字段
*/
public function _filterData($data)
{
$new_data = array();
$new_data['order_bn'] = $this->_getOrderBn($data);
$new_data['trade_no'] = isset($data['main_order_no']) ? $data['main_order_no'] : ''; // 业务主单据编码
$new_data['financial_no'] = isset($data['financial_no']) ? $data['financial_no'] : ''; // 唯一编码
$new_data['out_trade_no'] = isset($data['order_no']) ? $data['order_no'] : ''; // 二段LP单号
$new_data['trade_time'] = isset($data['business_time']) ? strtotime($data['business_time']) : 0;
$new_data['trade_type'] = isset($data['trade_type']) ? $data['trade_type'] : ''; // 结算方式
$new_data['money'] = isset($data['amount']) ? floatval($data['amount']) : 0; // 含税金额(保留正负)
$new_data['member'] = isset($data['supplier_name']) ? $data['supplier_name'] : '';
// 直接使用唯一编码作为唯一标识
$new_data['unique_id'] = isset($data['financial_no']) ? $data['financial_no'] : '';
$new_data['platform_type'] = 'miaosuda';
$new_data['remarks'] = isset($data['source_doc_type']) ? $data['source_doc_type'] : ''; // 来源单据类型
return $new_data;
}
/**
* 获取导入日期列
*/
public function getImportDateColunm($title = null)
{
// 找到时间列的位置
$timeColumn = array('账单创建时间', '业务时间', '交易支付时间');
$timeCol = array();
if ($title) {
foreach ($timeColumn as $v) {
$k = array_search($v, $title);
if ($k !== false) {
$timeCol[] = $k + 1;
}
}
}
$timezone = defined('DEFAULT_TIMEZONE') ? DEFAULT_TIMEZONE : 0;
return array('column' => $timeCol, 'time_diff' => $timezone * 3600);
}
/**
* 批量转换数据(可选,用于特殊数据处理)
*/
public function batchTransferData($data, $title) {
// 如果需要对整批数据进行预处理,可以在这里实现
// 例如:处理金额格式、清理特殊字符等
return $data;
}
}

View File

@@ -0,0 +1,460 @@
<?php
/**
* 天猫优品账单导入处理类
* 处理第一个工作表
*
* @author AI Assistant
* @version 1.0
*/
class financebase_data_bill_tmyp extends financebase_abstract_bill
{
public $order_bn_prefix = '';
public $column_num = 33; // 33个字段新增优品红包字段
public $ioTitle = array();
public $ioTitleKey = array();
public $verified_data = array();
public $shop_list_by_name = array(); // 店铺列表(按名称索引)
// 需要转换为行的金额列(列转行)
public $column_to_row = [
'amount', // 含税金额
'yp_red_packet', // 优品红包
];
// 指定要读取的工作表名称(已注释:现在只读取第一个工作表)
// public $sheet_name = '账单明细-寄售';
/**
* 获取标题映射
* @return array
*/
public function getTitle()
{
$title = array(
'bill_no' => '对账单号',
'settlement_company' => '结算公司主体',
'bill_create_time' => '账单生成时间',
'order_no' => '交易主单',
'trade_sub_order' => '交易子单',
'business_main_no' => '业务主单据编码',
'business_sub_no' => '业务子单据编码',
'pay_time' => '支付时间',
'business_time' => '业务时间',
'business_doc_type' => '业务单据类型',
'fee_type' => '费用类型',
'business_attr' => '经营属性',
'trade_type' => '结算方式',
'supplier_code' => '供应商编码',
'supplier_name' => '供应商名称',
'currency' => '结算币种',
'amount' => '含税金额',
'amount_without_tax' => '未税金额',
'tax_amount' => '税额',
'tax_rate' => '税率',
'goods_code' => '后端商品编码',
'goods_name' => '后端商品名称',
'unit' => '存储单位',
'quantity' => '商品数量',
'price_with_tax' => '含税单价',
'price_without_tax' => '未税单价',
'is_recalculate' => '是否重算',
'reference' => '参考',
'financial_no' => '唯一ID',
'settlement_no' => '结算流水编号',
'diff_type' => '差异判责类型',
'bill_category' => '账单分类',
'yp_red_packet' => '优品红包',
);
return $title;
}
/**
* 批量转换数据(列转行)
* 将含税金额和优品红包转换为两行数据
* @param array $data 原始数据
* @param array $title 标题行
* @return array
*/
public function batchTransferData($data, $title)
{
if (!$this->ioTitle) {
$this->ioTitle = $this->getTitle();
}
$titleKey = array();
foreach ($title as $k => $t) {
$titleKey[$k] = array_search($t, $this->getTitle());
}
$reData = [];
foreach($data as $row) {
$row = array_map('trim', $row);
// 跳过标题行
if($row[0] == '对账单号') {
continue;
}
// 过滤"票扣"数据:如果结算方式是"票扣",则忽略此行数据
$trade_type_index = array_search('trade_type', $titleKey);
if($trade_type_index !== false) {
$trade_type_value = isset($row[$trade_type_index]) ? trim($row[$trade_type_index]) : '';
if($trade_type_value == '票扣') {
continue;
}
}
$tmpRow = [];
foreach($row as $k => $v) {
if(isset($titleKey[$k]) && $titleKey[$k]) {
$tmpRow[$titleKey[$k]] = $v;
}
}
// 将含税金额和优品红包转换为单独的行数据
foreach($this->column_to_row as $col) {
// 检查金额是否存在、不为空、不为0包括 '0'、'0.00'、0、0.0 等)
if(isset($tmpRow[$col]) && $tmpRow[$col] !== '' && floatval($tmpRow[$col]) != 0) {
$tmp = $tmpRow;
$tmp['amount'] = $tmpRow[$col];
// 设置 trade_type
if($col == 'yp_red_packet') {
// 优品红包的 trade_type 就是"优品红包"
$tmp['trade_type'] = '优品红包';
// 优品红包需要重新生成 financial_no在原值基础上加 "-1"
if(isset($tmpRow['financial_no']) && $tmpRow['financial_no']) {
$tmp['financial_no'] = $tmpRow['financial_no'] . '-1';
}
} else {
// 含税金额的 trade_type 保持原来的结算方式
$tmp['trade_type'] = isset($tmpRow['trade_type']) ? $tmpRow['trade_type'] : '';
}
$reData[] = $tmp;
}
}
}
return $reData;
}
/**
* 处理数据(核心方法)
* @param array $row 数据行batchTransferData 返回的关联数组)
* @param int $offset 行偏移量
* @param array $title 标题行
* @return array
*/
public function getSdf($row, $offset=1, $title)
{
if(!$this->ioTitle){
$this->ioTitle = $this->getTitle();
$this->ioTitleKey = array_keys($this->ioTitle);
}
$res = array('status'=>true, 'data'=>array(), 'msg'=>'');
// batchTransferData 返回的是关联数组,直接处理
$tmp = array();
foreach($row as $k => $v) {
$tmp[$k] = trim($v, '\'');
}
// 过滤"票扣"数据:如果结算方式是"票扣",则忽略此行数据
if(isset($tmp['trade_type']) && $tmp['trade_type'] == '票扣') {
// 返回成功状态但不处理数据data为空
return array('status' => true, 'data' => array(), 'msg' => '');
}
// 验证必填字段
foreach ($tmp as $k => $v) {
// 必填字段:交易主单、结算方式、金额
if(in_array($k, array('order_no', 'trade_type', 'amount')))
{
if(!$v)
{
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 不能为空!", $offset, isset($this->ioTitle[$k]) ? $this->ioTitle[$k] : $k);
return $res;
}
}
// 时间格式验证
if(in_array($k, array('bill_create_time', 'pay_time', 'business_time')))
{
if($v && $v != '--') {
$result = finance_io_bill_verify::isDate($v);
if ($result['status'] == 'fail')
{
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 时间(%s)格式错误!", $offset, isset($this->ioTitle[$k]) ? $this->ioTitle[$k] : $k, $v);
return $res;
}
}
}
// 金额格式验证天猫优品支持多位小数如含税金额可能有6位小数
// 包含优品红包字段
if(in_array($k, array('amount', 'amount_without_tax', 'tax_amount', 'price_with_tax', 'price_without_tax', 'yp_red_packet')))
{
if($v) {
// 自定义金额验证:支持多位小数的正负数
if (!preg_match('/^-?\d+(\.\d+)?$/', $v)) {
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 金额(%s)格式错误!", $offset, isset($this->ioTitle[$k]) ? $this->ioTitle[$k] : $k, $v);
return $res;
}
}
}
// 特殊字段处理去除Excel导出的 ="" 包裹
if(in_array($k, array('order_no', 'business_main_no', 'business_sub_no', 'financial_no', 'settlement_no', 'goods_code'))){
$tmp[$k] = trim($v, '=\"');
}
}
$res['data'] = $tmp;
return $res;
}
/**
* 获取订单编号
* @param array $params
* @return string
*/
public function _getOrderBn($params)
{
// 直接返回交易主单order_no
if (isset($params['order_no']) && $params['order_no']) {
return $params['order_no'];
}
return '';
}
/**
* 过滤数据
* @param array $data
* @return array
*/
public function _filterData($data)
{
$new_data = array();
$new_data['order_bn'] = $this->_getOrderBn($data);
$new_data['trade_no'] = isset($data['order_no']) ? $data['order_no'] : ''; // 交易主单
$new_data['financial_no'] = isset($data['financial_no']) ? $data['financial_no'] : ''; // 唯一ID
$new_data['out_trade_no'] = isset($data['business_main_no']) ? $data['business_main_no'] : ''; // 业务主单据编码
$new_data['trade_time'] = isset($data['business_time']) ? strtotime($data['business_time']) : 0;
$new_data['trade_type'] = isset($data['trade_type']) ? $data['trade_type'] : ''; // 结算方式
$new_data['money'] = isset($data['amount']) ? floatval($data['amount']) : 0; // 含税金额(保留正负)
$new_data['member'] = ''; // 会员信息为空
// 使用唯一ID作为唯一标识
$new_data['unique_id'] = isset($data['financial_no']) ? $data['financial_no'] : '';
$new_data['platform_type'] = 'tmyp';
$new_data['remarks'] = isset($data['business_doc_type']) ? $data['business_doc_type'] : ''; // 业务单据类型
return $new_data;
}
/**
* 获取账单分类
* @param array $params
* @return string
*/
public function getBillCategory($params)
{
if (!$this->rules) {
$this->getRules('alipay'); // 使用alipay的规则
}
$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 '';
}
/**
* 同步到对账表finance.bill
* @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'] : '';
}
$base_sdf = array(
'order_bn' => $this->_getOrderBn($tmp),
'channel_id' => $data['shop_id'],
'channel_name' => $data['shop_name'],
'trade_time' => isset($tmp['business_time']) ? strtotime($tmp['business_time']) : 0,
'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' => isset($tmp['financial_no']) ? $tmp['financial_no'] : '', // 唯一ID
'member' => '', // 会员信息为空
'memo' => isset($tmp['remarks']) ? $tmp['remarks'] : '',
'unique_id' => $data['unique_id'],
'create_time' => time(),
'fee_type' => isset($tmp['trade_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|null $title
* @return array
*/
public function getImportDateColunm($title = null)
{
// 找到时间列的位置
$timeColumn = array('账单生成时间', '支付时间', '业务时间');
$timeCol = array();
if ($title) {
foreach ($timeColumn as $v) {
$k = array_search($v, $title);
if ($k !== false) {
$timeCol[] = $k + 1;
}
}
}
$timezone = defined('DEFAULT_TIMEZONE') ? DEFAULT_TIMEZONE : 0;
return array('column' => $timeCol, 'time_diff' => $timezone * 3600);
}
/**
* 更新订单号
* @param array $data 数据
* @return void
*/
public function updateOrderBn($data)
{
$this->_formatData($data);
$mdlBill = app::get('finance')->model('bill');
if(!$this->shop_list_by_name)
{
$this->shop_list_by_name = financebase_func::getShopList(financebase_func::getShopType());
$this->shop_list_by_name = array_column($this->shop_list_by_name, null, 'name');
}
foreach ($data as $v)
{
// 跳过标题行
if('对账单号' == $v[0]) continue;
// 需要有店铺名称、账单号、订单号才能更新
// 假设最后3列是店铺名称、账单号、订单号需要根据实际情况调整
// 注意:由于新增了优品红包字段,列索引需要相应调整
if(!isset($v[33]) || !isset($v[34]) || !isset($v[35])) continue;
if(!$v[33] || !$v[34] || !$v[35]) continue;
$shop_id = isset($this->shop_list_by_name[$v[33]]) ? $this->shop_list_by_name[$v[33]]['shop_id'] : 0;
if(!$shop_id) continue;
$filter = array('bill_bn' => $v[34], 'shop_id' => $shop_id);
// 找到unique_id
$bill_info = $mdlBill->getList('unique_id,bill_id', $filter, 0, 1);
if(!$bill_info) continue;
$bill_info = $bill_info[0];
if($mdlBill->update(array('order_bn' => $v[35]), array('bill_id' => $bill_info['bill_id'])))
{
app::get('financebase')->model('bill')->update(array('order_bn' => $v[35]), array('unique_id' => $bill_info['unique_id'], 'shop_id' => $shop_id));
$op_name = kernel::single('desktop_user')->get_name();
$content = sprintf("订单号改成:%s", $v[35]);
finance_func::addOpLog($v[34], $op_name, $content, '更新订单号');
}
}
}
/**
* 检查文件是否有效
* @param string $file_name 文件名
* @param string $file_type 文件类型
* @return array
*/
public function checkFile($file_name, $file_type)
{
if ($file_type !== 'xlsx') {
return array(false, '天猫优品导入只支持.xlsx格式的Excel文件');
}
$ioType = kernel::single('financebase_io_' . $file_type);
// 读取第一个工作表(不再限制特定工作表名称)
$row = $ioType->getData($file_name, 0, 1, 0);
if (empty($row) || !isset($row[0])) {
return array(false, '文件没有数据');
}
// 完整验证所有列名
$title = array_values($this->getTitle());
sort($title);
$tmypTitle = $row[0];
sort($tmypTitle);
// 完全匹配
if ($title == $tmypTitle) {
return array(true, '文件模板匹配', $row[0]);
}
// 包含关系匹配
if (!array_diff($tmypTitle, $title)) {
return array(true, '文件模板匹配', $row[0]);
}
return array(false, '文件模板错误:' . var_export($row[0], true) . ',正确的为:' . var_export($title, 1));
}
}

View File

@@ -0,0 +1,512 @@
<?php
/**
* 微盟零售账单导入处理类
* 支持正向(销售)和逆向(退款)两种模板
*
* @author AI Assistant
* @version 1.0
*/
class financebase_data_bill_weimobr extends financebase_abstract_bill
{
public $order_bn_prefix = '';
public $column_num = 0; // 动态字段数
public $ioTitle = array();
public $ioTitleKey = array();
public $verified_data = array();
public $shop_list_by_name = array(); // 店铺列表(按名称索引)
// 标记当前处理的是正向还是逆向
private $is_forward = null;
// 正向表头(销售)
private $title_forward = null;
// 逆向表头(退款)
private $title_reverse = null;
/**
* 获取正向表头(销售)
* @return array
*/
private function getTitleForward()
{
if ($this->title_forward === null) {
$this->title_forward = array(
'trade_time' => '交易时间',
'amount' => '交易金额',
'settlement_amount' => '应结订单金额',
'fee_amount' => '手续费金额',
'income_amount' => '入账金额',
'wechat_coupon' => '微信代金券金额',
'unionpay_coupon' => '云闪付优惠券金额',
'alipay_coupon' => '支付宝代金券商家优惠金额',
'alipay_amount' => '支付宝实收金额',
'reconciliation_status' => '对账状态',
'bill_date' => '账单日期',
'trade_type' => '交易类型',
'split_status' => '分账状态',
'unfreeze_time' => '解冻时间',
'settlement_status' => '结算状态',
'settlement_time' => '结算时间',
'shop_name' => '店铺名称',
'org_id' => '组织ID',
'parent_order_no' => '父单号',
'order_no' => '订单编号',
'trade_no' => '交易单号',
'channel_trade_no' => '通道交互单号',
'refund_amount' => '退款金额',
'fee_return' => '手续费返还',
'pay_mode' => '支付模式',
'merchant_no' => '支付商户号',
'currency' => '币种',
'business_type' => '业务类型',
'channel_type' => '渠道类型',
'remark1' => '备注1',
'payer_info' => '付款人信息',
);
}
return $this->title_forward;
}
/**
* 获取逆向表头(退款)
* @return array
*/
private function getTitleReverse()
{
if ($this->title_reverse === null) {
$this->title_reverse = array(
'refund_time' => '退款时间',
'amount' => '退款金额',
'actual_refund' => '实退金额',
'fee_return' => '手续费返还',
'wechat_coupon_refund' => '微信代金券金额退款',
'unionpay_coupon_refund' => '云闪付优惠券退款金额',
'refund_status' => '退款状态',
'remark' => '备注',
'reconciliation_status' => '对账状态',
'bill_date' => '账单日期',
'shop_name' => '店铺名称',
'org_id' => '组织ID',
'dispute_no' => '维权单号',
'channel_refund_no' => '通道退款单号',
'channel_trade_no' => '通道交互单号',
'original_trade_time' => '原交易时间',
'original_trade_amount' => '原交易金额',
'original_unfreeze_time' => '原交易解冻时间',
'original_parent_order' => '原父单号',
'order_no' => '原订单编号', // 统一使用 order_noExcel表头是"原订单编号"
'original_trade_no' => '原交易单号',
'pay_mode' => '支付模式',
'merchant_no' => '支付商户号',
'currency' => '币种',
'business_type' => '业务类型',
'channel_type' => '渠道类型',
);
}
return $this->title_reverse;
}
/**
* 获取标题映射(支持正向和逆向两种表头)
* @return array
*/
public function getTitle()
{
// 合并正向和逆向表头,支持两种模板
return array_merge($this->getTitleForward(), $this->getTitleReverse());
}
/**
* 处理数据(核心方法)
* @param array $row 数据行
* @param int $offset 行偏移量
* @param array $title 标题行
* @return array
*/
public function getSdf($row, $offset=1, $title)
{
$row = array_map('trim', $row);
// 判断是正向还是逆向(根据表头第一个字段)
if ($this->is_forward === null && !empty($title)) {
$first_title = trim($title[0]);
if ($first_title == '退款时间') {
$this->is_forward = false; // 逆向(退款)
} else {
$this->is_forward = true; // 正向(销售)
}
}
// 根据正向/逆向获取对应的表头
if ($this->is_forward) {
$current_title = $this->getTitleForward();
} else {
$current_title = $this->getTitleReverse();
}
if(!$this->ioTitle){
$this->ioTitle = $current_title;
$this->ioTitleKey = array_keys($this->ioTitle);
}
$titleKey = array();
foreach ($title as $k => $t) {
$titleKey[$k] = array_search($t, $current_title);
if ($titleKey[$k] === false) {
return array('status' => false, 'msg' => '未定义字段`' . $t . '`');
}
}
$res = array('status'=>true, 'data'=>array(), 'msg'=>'');
// 正向:跳过表头行(交易时间)
// 逆向:跳过表头行(退款时间)
$first_field = $this->is_forward ? '交易时间' : '退款时间';
if(count($row) > 0 && $row[0] != $first_field)
{
$tmp = array_combine($titleKey, $row);
// 验证必填字段
// 正向必填:交易时间、交易金额、订单编号
// 逆向必填:退款时间、退款金额、订单编号(原订单编号)
if($this->is_forward) {
$required_fields = array('trade_time', 'amount', 'order_no');
} else {
$required_fields = array('refund_time', 'amount', 'order_no');
}
foreach ($tmp as $k => $v) {
// 只验证必填字段
if(in_array($k, $required_fields))
{
if(!$v || $v == '--')
{
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 不能为空!", $offset, $this->ioTitle[$k]);
return $res;
}
// 必填字段的格式验证
// 时间字段格式验证
if(in_array($k, array('trade_time', 'refund_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;
}
}
// 金额字段格式验证
if($k == 'amount')
{
// 自定义金额验证:支持多位小数的正负数
if (!preg_match('/^-?\d+(\.\d+)?$/', $v)) {
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 金额(%s)格式错误!", $offset, $this->ioTitle[$k], $v);
return $res;
}
}
}
// 特殊字段处理去除Excel导出的 ="" 包裹
if(in_array($k, array('order_no', 'trade_no', 'channel_trade_no', 'original_trade_no', 'parent_order_no', 'original_parent_order'))){
$tmp[$k] = trim($v, '=\"');
}
}
// 标记数据类型(正向或逆向)
$tmp['_is_forward'] = $this->is_forward;
// 处理 trade_type正向为"交易金额",逆向为"退款金额"
if ($this->is_forward) {
$tmp['trade_type'] = '交易金额';
} else {
$tmp['trade_type'] = '退款金额';
}
// 处理 amount如果是逆向转为负数
if (isset($tmp['amount']) && !$this->is_forward) {
$tmp['amount'] = -abs(floatval($tmp['amount'])); // 逆向金额转为负数
} elseif (isset($tmp['amount'])) {
$tmp['amount'] = floatval($tmp['amount']); // 正向金额保持正数
}
$res['data'] = $tmp;
}
return $res;
}
/**
* 获取订单编号
* @param array $params
* @return string
*/
public function _getOrderBn($params)
{
// 正向和逆向都使用 order_no
if (isset($params['order_no']) && $params['order_no']) {
return $params['order_no'];
}
return '';
}
/**
* 过滤数据
* @param array $data
* @return array
*/
public function _filterData($data)
{
$order_bn = $this->_getOrderBn($data);
$is_forward = isset($data['_is_forward']) ? $data['_is_forward'] : true;
// 正逆向共用的字段
$new_data = array(
'order_bn' => $order_bn,
'trade_no' => $order_bn, // trade_no 都使用订单号
'financial_no' => isset($data['channel_trade_no']) ? $data['channel_trade_no'] : '', // financial_no 都使用 channel_trade_no
'member' => '', // member 都为空
'remarks' => '', // remarks 都为空
'trade_type' => isset($data['trade_type']) ? $data['trade_type'] : '', // trade_type 已经在 getSdf 中处理
'money' => isset($data['amount']) ? floatval($data['amount']) : 0, // amount 已经在 getSdf 中处理(逆向已转为负数)
);
// 正逆向不同的字段
if ($is_forward) {
// 正向(销售)
$new_data['out_trade_no'] = isset($data['trade_no']) ? $data['trade_no'] : '';
$new_data['trade_time'] = isset($data['trade_time']) ? strtotime($data['trade_time']) : 0;
} else {
// 逆向(退款)
$new_data['out_trade_no'] = isset($data['original_trade_no']) ? $data['original_trade_no'] : '';
$new_data['trade_time'] = isset($data['refund_time']) ? strtotime($data['refund_time']) : 0;
}
// 生成唯一ID使用 trade_no + financial_no + trade_time + money 的 MD5
$unique_str = sprintf(
'%s_%s_%s_%s',
$new_data['trade_no'],
$new_data['financial_no'],
$new_data['trade_time'],
$new_data['money']
);
$new_data['unique_id'] = md5($unique_str);
$new_data['platform_type'] = 'weimobr';
return $new_data;
}
/**
* 获取账单分类
* @param array $params
* @return string
*/
public function getBillCategory($params)
{
if (!$this->rules) {
$this->getRules('weimobr'); // 使用微信支付的规则
}
$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 '';
}
/**
* 同步到对账表finance.bill
* @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');
// 判断是正向还是逆向
$is_forward = isset($tmp['_is_forward']) ? $tmp['_is_forward'] : true;
$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'] : '';
}
// 根据正向/逆向获取时间(字符串转时间戳)
if ($is_forward) {
$trade_time = isset($tmp['trade_time']) ? strtotime($tmp['trade_time']) : 0;
} else {
$trade_time = isset($tmp['refund_time']) ? strtotime($tmp['refund_time']) : 0;
}
// amount 已经在 getSdf 中处理(逆向已转为负数)
$money = isset($tmp['amount']) ? floatval($tmp['amount']) : 0;
$credential_number = isset($tmp['channel_trade_no']) ? $tmp['channel_trade_no'] : '';
$base_sdf = array(
'order_bn' => $this->_getOrderBn($tmp),
'channel_id' => $data['shop_id'],
'channel_name' => $data['shop_name'],
'trade_time' => $trade_time,
'fee_obj' => $tmp['fee_obj'],
'money' => round($money, 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' => $credential_number,
'member' => '', // member 都为空
'memo' => '', // remarks 都为空
'unique_id' => $data['unique_id'],
'create_time' => time(),
'fee_type' => $is_forward ? '交易金额' : '退款金额',
'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|null $title
* @return array
*/
public function getImportDateColunm($title = null)
{
// 找到时间列的位置
// 正向使用"交易时间",逆向使用"退款时间"
$timeColumn = array();
if ($this->is_forward === false) {
$timeColumn = array('退款时间');
} else {
$timeColumn = array('交易时间');
}
$timeCol = array();
if ($title) {
foreach ($timeColumn as $v) {
$k = array_search($v, $title);
if ($k !== false) {
$timeCol[] = $k + 1;
}
}
}
$timezone = defined('DEFAULT_TIMEZONE') ? DEFAULT_TIMEZONE : 0;
return array('column' => $timeCol, 'time_diff' => $timezone * 3600);
}
/**
* 更新订单编号
* @param array $data
* @return bool
*/
public function updateOrderBn($data)
{
$mdlBill = app::get('financebase')->model('bill');
foreach($data as $row) {
$order_bn = $this->_getOrderBn($row);
if($order_bn) {
$is_forward = isset($row['_is_forward']) ? $row['_is_forward'] : true;
$filter = array(
'unique_id' => md5(sprintf(
'%s_%s',
isset($row['order_no']) ? $row['order_no'] : '',
$is_forward ? (isset($row['channel_trade_no']) ? $row['channel_trade_no'] : '') : (isset($row['channel_refund_no']) ? $row['channel_refund_no'] : '')
))
);
$mdlBill->update(array('order_bn' => $order_bn), $filter);
}
}
return true;
}
/**
* 检查文件是否有效
* @param string $file_name 文件名
* @param string $file_type 文件类型
* @return array
*/
public function checkFile($file_name, $file_type)
{
$ioType = kernel::single('financebase_io_' . $file_type);
// 读取第一个工作表的第一行(表头)
$row = $ioType->getData($file_name, 0, 1, 0);
if (empty($row) || !isset($row[0])) {
return array(false, '文件没有数据');
}
// 判断是正向还是逆向(根据表头第一个字段)
$first_title = trim($row[0][0]);
$is_forward = ($first_title != '退款时间');
// 根据正向/逆向获取对应的表头进行验证
if ($is_forward) {
$title = array_values($this->getTitleForward());
} else {
$title = array_values($this->getTitleReverse());
}
sort($title);
$fileTitle = $row[0];
sort($fileTitle);
// 完全匹配
if ($title == $fileTitle) {
return array(true, '文件模板匹配', $row[0]);
}
// 包含关系匹配
if (!array_diff($fileTitle, $title)) {
return array(true, '文件模板匹配', $row[0]);
}
return array(false, '文件模板错误:' . var_export($row[0], true) . ',正确的为:' . var_export($title, 1));
}
}

View File

@@ -0,0 +1,251 @@
<?php
/**
* 处理小红书账单导入
*
* @author AI Assistant
* @version 1.0
*/
class financebase_data_bill_xhs extends financebase_abstract_bill
{
public $column_num = 5; // 5个字段订单号、国补金额、SKU、数量、账单日期
public $ioTitle = array();
public $ioTitleKey = array();
private static $shop_type_cache = array(); // 静态变量缓存店铺类型数据
public function getTitle()
{
$title = array(
'order_bn' => '订单号',
'amount' => '国补金额',
'sku' => 'SKU',
'quantity' => '数量',
'trade_time' => '账单日期'
);
return $title;
}
public function getSdf($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);
// 必填字段验证
$required_fields = array('order_bn', 'amount', 'sku', 'quantity', 'trade_time');
foreach ($required_fields as $field) {
if (empty($tmp[$field])) {
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 不能为空!", $offset, $this->ioTitle[$field]);
return $res;
}
}
// 金额格式验证
if (!is_numeric($tmp['amount'])) {
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 金额(%s)格式错误!", $offset, $this->ioTitle['amount'], $tmp['amount']);
return $res;
}
// 数量格式验证
if (!is_numeric($tmp['quantity']) || $tmp['quantity'] <= 0) {
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 数量(%s)必须为正数!", $offset, $this->ioTitle['quantity'], $tmp['quantity']);
return $res;
}
// 日期格式验证
if (!strtotime($tmp['trade_time'])) {
$res['status'] = false;
$res['msg'] = sprintf("LINE %d : %s 日期(%s)格式错误!", $offset, $this->ioTitle['trade_time'], $tmp['trade_time']);
return $res;
}
$res['data'] = $tmp;
}
return $res;
}
public function _filterData($data)
{
$new_data = array();
$new_data['order_bn'] = $this->_getOrderBn($data);
$new_data['trade_no'] = '';
$new_data['financial_no'] = $data['order_bn'];
$new_data['out_trade_no'] = '';
$new_data['trade_time'] = strtotime($data['trade_time']);
$new_data['trade_type'] = '小红书账单';
$new_data['money'] = $data['amount'];
$new_data['member'] = '';
// unique_id 生成规则shop_id + 订单号 + SKU + 国补金额的MD5
$shop_id = $data['shop_id'] ?: $this->getCurrentShopId();
$new_data['unique_id'] = md5($shop_id . '-' . $data['order_bn'] . '-' . $data['sku'] . '-' . $data['amount']);
$shop_info = $this->getShopInfoByShopId($shop_id);
$new_data['platform_type'] = 'xhs'; // 小红书固定平台类型
$new_data['remarks'] = '小红书账单导入-SKU:' . $data['sku'] . ' 数量:' . $data['quantity'];
return $new_data;
}
public function _getOrderBn($params)
{
return $params['order_bn'];
}
public function getBillCategory($params)
{
// 小红书账单固定类别
return '小红书账单';
}
/**
* 检查文件是否有效
* @param string $file_name 文件名
* @param string $file_type 文件类型
* @return array [是否有效, 错误信息, 标题]
*/
public function checkFile($file_name, $file_type)
{
$ioType = kernel::single('financebase_io_' . $file_type);
$row = $ioType->getData($file_name, 0, 1);
$title = array_values($this->getTitle());
sort($title);
$xhsTitle = $row[0];
sort($xhsTitle);
if ($title == $xhsTitle) {
return array(true, '文件模板匹配', $row[0]);
}
if (!array_diff($xhsTitle, $title)) {
return array(true, '文件模板匹配', $row[0]);
}
return array(false, '文件模板错误:' . var_export($row[0], true) . ',正确的为:' . var_export($title, 1));
}
/**
* 获取导入日期列配置
* @param array $title 标题数组
* @return array
*/
public function getImportDateColunm($title = null)
{
// 小红书导入的日期字段配置
$timeColumn = ['账单日期'];
$timeCol = array();
foreach ($timeColumn as $v) {
if($k = array_search($v, $title)) {
$timeCol[] = $k+1;
}
}
$timezone = defined('DEFAULT_TIMEZONE') ? DEFAULT_TIMEZONE : 0;
return array('column' => $timeCol, 'time_diff' => $timezone * 3600);
}
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'] = '小红书账单';
$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'] : '';
}
$base_sdf = array(
'order_bn' => $tmp['order_bn'],
'channel_id' => $data['shop_id'],
'channel_name' => $data['shop_name'],
'trade_time' => strtotime($tmp['trade_time']),
'fee_obj' => $tmp['fee_obj'],
'money' => round($tmp['money'], 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(),
'monthly_id' => 0,
'monthly_item_id' => 0,
'monthly_status' => 0,
'crc32_order_bn' => sprintf('%u', crc32($tmp['order_bn'])),
'bill_bn' => $mdlBill->gen_bill_bn(),
'unconfirm_money' => round($tmp['money'], 2),
);
if ($mdlBill->insert($base_sdf)) {
kernel::single('finance_monthly_report_items')->dealBillMatchReport($base_sdf['bill_id']);
return true;
}
return false;
}
private function getShopInfoByShopId($shop_id)
{
if (!$shop_id) {
return null;
}
// 如果缓存为空,一次性查询所有店铺数据
if (empty(self::$shop_type_cache)) {
$mdlShop = app::get('ome')->model('shop');
$shop_list = $mdlShop->getList('shop_id,shop_type');
// 将数据存入静态缓存key为shop_idvalue为shop数组
foreach ($shop_list as $shop) {
self::$shop_type_cache[$shop['shop_id']] = $shop;
}
}
// 从缓存中获取shop信息
if (isset(self::$shop_type_cache[$shop_id])) {
return self::$shop_type_cache[$shop_id];
}
return null;
}
private function getCurrentShopId()
{
// 从当前请求中获取shop_id
return $_POST['shop_id'] ?? '';
}
}