新增
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\command;
|
||||
|
||||
use app\adminapi\logic\order\OrderLogic;
|
||||
use think\console\Command;
|
||||
use think\console\Input;
|
||||
use think\console\input\Argument;
|
||||
use think\console\input\Option;
|
||||
use think\console\Output;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 同步企业微信对外收款到订单
|
||||
* 按天拉取,避免接口时间范围限制
|
||||
*/
|
||||
class SyncWechatWorkBills extends Command
|
||||
{
|
||||
protected function configure()
|
||||
{
|
||||
$this->setName('sync_wechat_work_bills')
|
||||
->setDescription('同步企业微信对外收款到订单(按天拉取)')
|
||||
->addOption('date', 'd', Option::VALUE_OPTIONAL, '指定日期 Y-m-d,默认今天', '')
|
||||
->addOption('days', null, Option::VALUE_OPTIONAL, '拉取最近N天,每天单独请求', '1');
|
||||
}
|
||||
|
||||
protected function execute(Input $input, Output $output)
|
||||
{
|
||||
$dateStr = $input->getOption('date');
|
||||
$days = (int)$input->getOption('days');
|
||||
$days = $days > 0 ? min($days, 31) : 1;
|
||||
|
||||
$totalCreated = 0;
|
||||
$totalUpdated = 0;
|
||||
$totalSkipped = 0;
|
||||
$errors = [];
|
||||
|
||||
if ($dateStr) {
|
||||
$dates = [$dateStr];
|
||||
} else {
|
||||
$dates = [];
|
||||
for ($i = 0; $i < $days; $i++) {
|
||||
$dates[] = date('Y-m-d', strtotime("-{$i} days"));
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($dates as $d) {
|
||||
$beginTime = strtotime($d . ' 00:00:00');
|
||||
$endTime = strtotime($d . ' 23:59:59');
|
||||
$output->writeln("同步 {$d} ...");
|
||||
$result = OrderLogic::syncFromWechatWorkBills($beginTime, $endTime);
|
||||
$totalCreated += $result['created'];
|
||||
$totalUpdated += $result['updated'];
|
||||
$totalSkipped += $result['skipped'];
|
||||
if (!empty($result['errors'])) {
|
||||
$errors = array_merge($errors, $result['errors']);
|
||||
}
|
||||
$output->writeln(" -> 新增 {$result['created']} 更新 {$result['updated']} 跳过 {$result['skipped']}");
|
||||
}
|
||||
|
||||
$output->writeln("完成: 共新增 {$totalCreated} 更新 {$totalUpdated} 跳过 {$totalSkipped}");
|
||||
if (!empty($errors)) {
|
||||
foreach ($errors as $e) {
|
||||
$output->writeln("<error>{$e}</error>");
|
||||
Log::error('sync_wechat_work_bills: ' . $e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\service\wechat;
|
||||
|
||||
use think\facade\Cache;
|
||||
use think\facade\Log;
|
||||
|
||||
/**
|
||||
* 企业微信对外收款服务
|
||||
* 参考 https://developer.work.weixin.qq.com/document/path/93727
|
||||
*/
|
||||
class WechatWorkExternalPayService
|
||||
{
|
||||
private string $corpId;
|
||||
private string $secret;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$config = config('pay.wechat_work', []);
|
||||
$this->corpId = (string)($config['corp_id'] ?? '');
|
||||
$this->secret = (string)($config['external_pay_secret'] ?? $config['secret'] ?? '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 access_token(对外收款应用)
|
||||
*/
|
||||
public function getAccessToken(): ?string
|
||||
{
|
||||
if (empty($this->corpId) || empty($this->secret)) {
|
||||
Log::error('企业微信对外收款:corp_id 或 external_pay_secret 未配置');
|
||||
return null;
|
||||
}
|
||||
|
||||
$cacheKey = 'work_wechat_external_pay_token_' . md5($this->corpId . $this->secret);
|
||||
$cached = Cache::get($cacheKey);
|
||||
if ($cached) {
|
||||
return $cached;
|
||||
}
|
||||
|
||||
$url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={$this->corpId}&corpsecret={$this->secret}";
|
||||
$response = $this->httpGet($url);
|
||||
|
||||
if (!$response || ($response['errcode'] ?? -1) != 0) {
|
||||
Log::error('企业微信对外收款获取access_token失败: ' . json_encode($response));
|
||||
return null;
|
||||
}
|
||||
|
||||
$accessToken = $response['access_token'];
|
||||
Cache::set($cacheKey, $accessToken, ($response['expires_in'] ?? 7200) - 200);
|
||||
return $accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取对外收款记录
|
||||
* @param int $beginTime 开始时间戳(秒)
|
||||
* @param int $endTime 结束时间戳(秒)
|
||||
* @param string $payeeUserid 收款人userid,空则查全部
|
||||
* @param string $cursor 分页游标
|
||||
* @param int $limit 最大记录数,最大1000
|
||||
* @return array
|
||||
*/
|
||||
public function getBillList(int $beginTime, int $endTime, string $payeeUserid = '', string $cursor = '', int $limit = 100): array
|
||||
{
|
||||
$accessToken = $this->getAccessToken();
|
||||
if (!$accessToken) {
|
||||
return ['errcode' => -1, 'errmsg' => '获取access_token失败', 'bill_list' => []];
|
||||
}
|
||||
|
||||
$url = "https://qyapi.weixin.qq.com/cgi-bin/externalpay/get_bill_list?access_token={$accessToken}";
|
||||
$data = [
|
||||
'begin_time' => $beginTime,
|
||||
'end_time' => $endTime,
|
||||
'limit' => $limit,
|
||||
];
|
||||
if ($payeeUserid) {
|
||||
$data['payee_userid'] = $payeeUserid;
|
||||
}
|
||||
if ($cursor) {
|
||||
$data['cursor'] = $cursor;
|
||||
}
|
||||
|
||||
$response = $this->httpPost($url, $data);
|
||||
if (!$response) {
|
||||
return ['errcode' => -1, 'errmsg' => '请求失败', 'bill_list' => []];
|
||||
}
|
||||
|
||||
$billList = $response['bill_list'] ?? [];
|
||||
return [
|
||||
'errcode' => $response['errcode'] ?? 0,
|
||||
'errmsg' => $response['errmsg'] ?? '',
|
||||
'bill_list' => is_array($billList) ? $billList : [],
|
||||
'next_cursor' => $response['next_cursor'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
private function httpGet(string $url)
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
$result = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $result ? json_decode($result, true) : null;
|
||||
}
|
||||
|
||||
private function httpPost(string $url, array $data)
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $url);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 10);
|
||||
$result = curl_exec($ch);
|
||||
curl_close($ch);
|
||||
return $result ? json_decode($result, true) : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user