- 框架初始化
 - 安装插件
 - 修复PHP8.4报错
This commit is contained in:
2025-04-19 17:21:20 +08:00
commit c6a4e1f5f6
5306 changed files with 967782 additions and 0 deletions

View File

@@ -0,0 +1,49 @@
<?php
namespace addons\shopro\service;
use app\admin\model\shopro\SearchHistory as SearchHistoryModel;
use app\admin\model\shopro\user\User;
class SearchHistory
{
protected $user = null;
/**
* @var array
*/
public $config = [];
public function __construct($user = null)
{
$this->user = is_numeric($user) ? User::get($user) : $user;
$this->user = $this->user ?: auth_user();
}
public function save($params)
{
$keyword = $params['keyword'];
// SearchHistoryModel::where('user_id', $this->user->id)->where('keyword', $keyword)->delete();
$searchHistory = new SearchHistoryModel();
$searchHistory->user_id = $this->user ? $this->user->id : 0;
$searchHistory->keyword = $keyword;
$searchHistory->save();
}
public function hotSearch()
{
$now = time();
$start = time() - (86400 * 30); // 30 天前
$hotSearchs = SearchHistoryModel::field('keyword,count(*) as num')
->whereTime('createtime', 'between', [$start, $now])
->group('keyword')->order('num', 'desc')->limit(5)->select();
return $hotSearchs;
}
}

View File

@@ -0,0 +1,311 @@
<?php
namespace addons\shopro\service;
use app\admin\model\shopro\goods\Goods;
use app\admin\model\shopro\goods\SkuPrice;
use app\admin\model\shopro\app\ScoreSkuPrice;
use app\admin\model\shopro\activity\SkuPrice as ActivitySkuPriceModel;
use app\admin\model\shopro\order\OrderItem;
use addons\shopro\facade\ActivityRedis as ActivityRedisFacade;
use addons\shopro\facade\Redis;
use addons\shopro\traits\StockWarning;
/**
* 库存销量
*/
class StockSale
{
use StockWarning;
/**
* 下单锁定库存
*
* @param array $buyInfo
* @return void
*/
public function stockLock($buyInfo)
{
$goods = $buyInfo['goods'];
// 普通商品还是积分商品
$order_type = request()->param('order_type', '');
$order_type = $order_type ?: 'goods';
$buy_num = $buyInfo['goods_num'];
// 记录缓存的 hash key
$keyCacheStockHash = $this->getCacheStockHashKey();
if ($order_type == 'score') {
// 普通商品销量处理 (积分和普通商品的缓存 key 不相同)
$keyScoreGoodsLockedNum = $this->getLockedGoodsKey($buyInfo['goods_id'], $buyInfo['goods_sku_price_id'], 'score');
$score_locked_num = redis_cache($keyScoreGoodsLockedNum);
// 验证积分商品库存是否充足(mysql 悲观锁此方法可靠但如果大规模秒杀容易mysql 死锁,请将商品添加为秒杀)
$stock = ScoreSkuPrice::where('goods_id', $buyInfo['goods_id'])->where('goods_sku_price_id', $buyInfo['current_sku_price']->id)->lock(true)->value('stock');
if (($stock - $score_locked_num) < $buy_num) {
error_stop('积分商品库存不足');
}
// 锁积分库存
redis_cache()->INCRBY($keyScoreGoodsLockedNum, $buy_num);
// 记录已经缓存的库存的key,如果下单出现异常,将所有锁定的库存退回
redis_cache()->HSET($keyCacheStockHash, $keyScoreGoodsLockedNum, $buy_num);
}
// 普通商品销量处理 (积分和普通商品的缓存 key 不相同)
$keyGoodsLockedNum = $this->getLockedGoodsKey($buyInfo['goods_id'], $buyInfo['goods_sku_price_id'], 'goods');
$locked_num = redis_cache($keyGoodsLockedNum);
// 验证商品库存是否充足(mysql 悲观锁此方法可靠但如果大规模秒杀容易mysql 死锁,请将商品添加为秒杀)
$stock = SkuPrice::where('id', $buyInfo['current_sku_price']->id)->lock(true)->value('stock');
if (($stock - $locked_num) < $buy_num) {
error_stop('商品库存不足');
}
// 锁库存
redis_cache()->INCRBY($keyGoodsLockedNum, $buy_num);
// 记录已经缓存的库存的key,如果下单出现异常,将所有锁定的库存退回
redis_cache()->HSET($keyCacheStockHash, $keyGoodsLockedNum, $buy_num);
}
/**
* 下单中断释放锁定的库存
*
* @return void
*/
public function faildStockUnLock()
{
// 记录缓存的 hash key
$keyCacheStockHash = $this->getCacheStockHashKey();
$cacheStocks = redis_cache()->HGETALL($keyCacheStockHash);
foreach ($cacheStocks as $key => $num) {
$this->unLockCache($key, $num);
}
redis_cache()->delete($keyCacheStockHash);
}
/**
* 下单成功,删除锁定库存标记
*
* @return void
*/
public function successDelHashKey()
{
// 记录缓存的 hash key
$keyCacheStockHash = $this->getCacheStockHashKey();
redis_cache()->delete($keyCacheStockHash);
}
/**
* 库存解锁
*/
public function stockUnLock($order)
{
$items = $order->items;
foreach ($items as $key => $item) {
$this->stockUnLockItem($order, $item);
}
}
public function stockUnLockItem($order, $item)
{
if ($order['type'] == 'score') {
$keyScoreGoodsLockedNum = $this->getLockedGoodsKey($item['goods_id'], $item['goods_sku_price_id'], 'score');
$this->unLockCache($keyScoreGoodsLockedNum, $item->goods_num);
}
$keyGoodsLockedNum = $this->getLockedGoodsKey($item['goods_id'], $item['goods_sku_price_id'], 'goods');
$this->unLockCache($keyGoodsLockedNum, $item->goods_num);
}
private function unLockCache($key, $num)
{
$locked_num = redis_cache()->DECRBY($key, $num);
if ($locked_num < 0) {
$locked_num = redis_cache()->set($key, 0);
}
}
// 真实正向 减库存加销量(支付成功扣库存,加销量)
public function forwardStockSale($order) {
$items = OrderItem::where('order_id', $order['id'])->select();
foreach ($items as $key => $item) {
// 增加商品销量
Goods::where('id', $item->goods_id)->setInc('sales', $item->goods_num);
$skuPrice = SkuPrice::where('id', $item->goods_sku_price_id)->find();
if ($skuPrice) {
SkuPrice::where('id', $item->goods_sku_price_id)->setDec('stock', $item->goods_num); // 减少库存
SkuPrice::where('id', $item->goods_sku_price_id)->setInc('sales', $item->goods_num); // 增加销量
// 库存预警检测
$skuPrice = SkuPrice::where('id', $item->goods_sku_price_id)->find();
$this->checkStockWarning($skuPrice);
}
if ($item->item_goods_sku_price_id) {
if ($order['type'] == 'score') {
// 积分商城商品,扣除积分规格库存
ScoreSkuPrice::where('id', $item->item_goods_sku_price_id)->setDec('stock', $item->goods_num); // 减少库存
ScoreSkuPrice::where('id', $item->item_goods_sku_price_id)->setInc('sales', $item->goods_num);
} else {
// 扣除活动库存
ActivitySkuPriceModel::where('id', $item->item_goods_sku_price_id)->setDec('stock', $item->goods_num); // 减少库存
ActivitySkuPriceModel::where('id', $item->item_goods_sku_price_id)->setInc('sales', $item->goods_num);
}
}
// 真实库存已减,库存解锁(非活动)
if (!$item['activity_id']) {
$this->stockUnLockItem($order, $item);
}
}
}
// 真实反向 加库存减销量(订单退全款)
public function backStockSale($order, $items = [])
{
if (!$items) {
$items = OrderItem::where('order_id', $order['id'])->select();
}
foreach ($items as $key => $item) {
// 返还商品销量
Goods::where('id', $item->goods_id)->setDec('sales', $item->goods_num);
// 返还规格库存
$skuPrice = SkuPrice::where('id', $item->goods_sku_price_id)->find();
if ($skuPrice) {
SkuPrice::where('id', $item->goods_sku_price_id)->setInc('stock', $item->goods_num); // 返还库存
SkuPrice::where('id', $item->goods_sku_price_id)->setDec('sales', $item->goods_num); // 减少销量
// 库存预警检测
$skuPrice = SkuPrice::where('id', $item->goods_sku_price_id)->find();
$this->checkStockWarning($skuPrice);
}
if ($item->item_goods_sku_price_id) {
if ($order['type'] == 'score') {
// 积分商城商品,扣除积分规格库存
ScoreSkuPrice::where('id', $item->item_goods_sku_price_id)->setInc('stock', $item->goods_num); // 返还库存
ScoreSkuPrice::where('id', $item->item_goods_sku_price_id)->setDec('sales', $item->goods_num); // 减少销量
} else {
// 扣除活动库存
ActivitySkuPriceModel::where('id', $item->item_goods_sku_price_id)->setInc('stock', $item->goods_num); // 返还库存
ActivitySkuPriceModel::where('id', $item->item_goods_sku_price_id)->setDec('sales', $item->goods_num); // 减少销量
}
}
}
}
// cache 正向加销量,添加订单之前拦截
public function cacheForwardSale($buyInfo) {
$goods = $buyInfo['goods'];
$activity = $goods['activity'];
if (has_redis()) {
$keys = ActivityRedisFacade::keysActivity([
'goods_id' => $goods->id,
'goods_sku_price_id' => $buyInfo['current_sku_price']->id,
], [
'activity_id' => $activity['id'],
'activity_type' => $activity['type'],
]);
extract($keys);
// 活动商品规格
$goodsSkuPrice = Redis::HGET($keyActivity, $keyGoodsSkuPrice);
$goodsSkuPrice = json_decode($goodsSkuPrice, true);
// 活动商品库存
$stock = $goodsSkuPrice['stock'] ?? 0;
// 当前销量 + 购买数量 salekey 如果不存在,自动创建
$sale = Redis::HINCRBY($keyActivity, $keySale, $buyInfo['goods_num']);
if ($sale > $stock) {
$sale = Redis::HINCRBY($keyActivity, $keySale, -$buyInfo['goods_num']);
error_stop('活动商品库存不足');
}
}
}
// cache 反向减销量,取消订单/订单自动关闭 时候
public function cacheBackSale($order) {
$items = OrderItem::where('order_id', $order['id'])->select();
foreach ($items as $key => $item) {
$this->cacheBackSaleByItem($item);
}
}
// 通过 OrderItem 减预库存
private function cacheBackSaleByItem($item)
{
if (has_redis()) {
$keys = ActivityRedisFacade::keysActivity([
'goods_id' => $item['goods_id'],
'goods_sku_price_id' => $item['goods_sku_price_id'],
], [
'activity_id' => $item['activity_id'],
'activity_type' => $item['activity_type'],
]);
extract($keys);
if (Redis::EXISTS($keyActivity) && Redis::HEXISTS($keyActivity, $keySale)) {
$sale = Redis::HINCRBY($keyActivity, $keySale, -$item['goods_num']);
}
return true;
}
}
/**
* 获取库存锁定 key
*
* @param int $goods_id
* @param int $goods_sku_price_id
* @return string
*/
private function getLockedGoodsKey($goods_id, $goods_sku_price_id, $order_type = 'goods')
{
$prefix = 'locked_goods_num:' . $order_type . ':' . $goods_id . ':' . $goods_sku_price_id;
return $prefix;
}
private function getCacheStockHashKey()
{
$params = request()->param();
$goodsList = $params['goods_list'] ?? [];
$key = client_unique() . ':' . json_encode($goodsList);
return md5($key);
}
}

View File

@@ -0,0 +1,89 @@
<?php
namespace addons\shopro\service;
use think\exception\HttpResponseException;
use app\admin\model\shopro\user\User as UserModel;
use app\admin\model\shopro\user\WalletLog;
use addons\shopro\library\Operator;
use app\common\model\MoneyLog;
use app\common\model\ScoreLog;
class Wallet
{
/**
* @name 变更会员资金
* @param int|object $user 会员对象或会员ID
* @param string $type 变更类型:money=余额,commission=佣金,score=积分
* @param float|string $amount 变更数量
* @param array $ext 扩展字段
* @param string $memo 备注
* @return boolean
*/
public static function change($user, $type, $amount, $event, $ext = [], $memo = '')
{
// 判断用户
if (is_numeric($user)) {
$user = UserModel::getById($user);
}
if (!$user) {
error_stop('未找到用户');
}
// 判断金额
if ($amount == 0) {
error_stop('请输入正确的金额');
}
if (!in_array($type, ['money', 'score', 'commission'])) {
error_stop('参数错误');
}
$before = $user->$type;
$after = bcadd((string)$user->$type, (string)$amount, 2);
// 只有后台扣除用户余额、扣除用户积分、佣金退回,钱包才可以是负值
if ($after < 0 && !in_array($event, ['admin_recharge', 'reward_back'])) {
$walletTypeText = WalletLog::TYPE_MAP[$type];
error_stop("可用{$walletTypeText}不足");
}
try {
// 更新会员余额信息
$user->setInc($type, $amount);
// 获取操作人
$oper = Operator::get();
// 写入日志
$walletLog = WalletLog::create([
'user_id' => $user->id,
'amount' => $amount,
'type' => $type,
'before' => $before,
'after' => $after,
'event' => $event,
'memo' => $memo,
'ext' => $ext,
'oper_type' => $oper['type'],
'oper_id' => $oper['id']
]);
// 钱包和积分记录存到 fastadmin 钱包积分记录表
if (in_array($type, ['money', 'score'])) {
$eventMap = (new WalletLog)->getEventMap();
$memo = $memo ?: $eventMap[$type][$event] ?? '';
if ($type === 'money') {
MoneyLog::create(['user_id' => $user->id, 'money' => $amount, 'before' => $before, 'after' => $after, 'memo' => $memo]);
} else if ($type === 'score') {
ScoreLog::create(['user_id' => $user->id, 'score' => $amount, 'before' => $before, 'after' => $after, 'memo' => $memo]);
}
}
// 账户变动事件
$data = ['walletLog' => $walletLog, 'type' => $type];
\think\Hook::listen('user_wallet_change', $data);
} catch (HttpResponseException $e) {
$data = $e->getResponse()->getData();
$message = $data ? ($data['msg'] ?? '') : $e->getMessage();
error_stop($message);
} catch (\Exception $e) {
error_stop('您提交的数据不正确');
}
return true;
}
}

View File

@@ -0,0 +1,326 @@
<?php
namespace addons\shopro\service;
use addons\shopro\exception\ShoproException;
use think\Db;
use think\Log;
use think\exception\HttpResponseException;
use app\admin\model\shopro\Withdraw as WithdrawModel;
use app\admin\model\shopro\WithdrawLog as WithdrawLogModel;
use addons\shopro\library\Operator;
use app\admin\model\shopro\ThirdOauth;
use addons\shopro\service\Wallet as WalletService;
use addons\shopro\library\pay\PayService;
use app\admin\model\shopro\user\User;
class Withdraw
{
protected $user = null;
/**
* @var array
*/
public $config = [];
public function __construct($user)
{
$this->user = is_numeric($user) ? User::get($user) : $user;
// 提现规则
$config = sheep_config('shop.recharge_withdraw.withdraw');
$config['min_amount'] = $config['min_amount'] == 0 ? $config['min_amount'] : number_format(floatval($config['min_amount']), 2, '.', '');
$config['max_amount'] = $config['max_amount'] == 0 ? $config['max_amount'] : number_format(floatval($config['max_amount']), 2, '.', '');
$config['charge_rate_format'] = round(floatval($config['charge_rate']), 1); // 1 位小数
$config['charge_rate'] = round((floatval($config['charge_rate']) * 0.01), 3);
$this->config = $config;
}
private function checkApply($type, $money, $charge)
{
if (!in_array($type, $this->config['methods'])) {
error_stop('暂不支持该提现方式');
}
if ($money <= 0) {
error_stop('请输入正确的提现金额');
}
// 检查最小提现金额
if ($this->config['min_amount'] > 0 && $money < $this->config['min_amount']) {
error_stop('提现金额不能少于 ' . $this->config['min_amount'] . '元');
}
// 检查最大提现金额
if ($this->config['max_amount'] > 0 && $money > $this->config['max_amount']) {
error_stop('提现金额不能大于 ' . $this->config['max_amount'] . '元');
}
if ($this->user->commission < bcadd($charge, $money, 2)) {
error_stop('可提现佣金不足');
}
// 检查最大提现次数
if (isset($this->config['max_num']) && $this->config['max_num'] > 0) {
$start_time = $this->config['num_unit'] == 'day' ? strtotime(date('Y-m-d', time())) : strtotime(date('Y-m', time()));
$num = WithdrawModel::where('user_id', $this->user->id)->where('createtime', '>=', $start_time)->count();
if ($num >= $this->config['max_num']) {
error_stop('每' . ($this->config['num_unit'] == 'day' ? '日' : '月') . '提现次数不能大于 ' . $this->config['max_num'] . '次');
}
}
}
public function accountInfo($type, $params)
{
$platform = request()->header('platform');
switch ($type) {
case 'wechat':
if ($platform == 'App') {
$platform = 'openPlatform';
} elseif (in_array($platform, ['WechatOfficialAccount', 'WechatMiniProgram'])) {
$platform = lcfirst(str_replace('Wechat', '', $platform));
}
$thirdOauth = ThirdOauth::where('provider', 'wechat')->where('platform', $platform)->where('user_id', $this->user->id)->find();
if (!$thirdOauth) {
error_stop('请先绑定微信账号', -1);
}
$withdrawInfo = [
'真实姓名' => $params['account_name'],
'微信用户' => $thirdOauth['nickname'],
'微信ID' => $thirdOauth['openid'],
];
break;
case 'alipay':
$withdrawInfo = [
'真实姓名' => $params['account_name'],
'支付宝账户' => $params['account_no']
];
break;
case 'bank':
$withdrawInfo = [
'真实姓名' => $params['account_name'],
'开户行' => $params['account_header'] ?? '',
'银行卡号' => $params['account_no']
];
break;
}
if (!isset($withdrawInfo)) {
error_stop('您的提现信息有误');
}
return $withdrawInfo;
}
public function apply($params)
{
$type = $params['type'] ?? 'wechat';
$money = $params['money'] ?? 0;
$money = (string)$money;
// 手续费
$charge = bcmul($money, (string)$this->config['charge_rate'], 2);
// 检查提现规则
$this->checkApply($type, $money, $charge);
// 获取账号信息
$withdrawInfo = $this->accountInfo($type, $params);
$withdraw = Db::transaction(function () use ($type, $money, $charge, $withdrawInfo) {
$platform = request()->header('platform');
// 添加提现记录
$withdraw = new WithdrawModel();
$withdraw->user_id = $this->user->id;
$withdraw->amount = $money;
$withdraw->charge_fee = $charge;
$withdraw->charge_rate = $this->config['charge_rate'];
$withdraw->withdraw_sn = get_sn($this->user->id, 'W');
$withdraw->withdraw_type = $type;
$withdraw->withdraw_info = $withdrawInfo;
$withdraw->status = 0;
$withdraw->platform = $platform;
$withdraw->save();
// 佣金钱包变动
WalletService::change($this->user, 'commission', - bcadd($charge, $money, 2), 'withdraw', [
'withdraw_id' => $withdraw->id,
'amount' => $withdraw->amount,
'charge_fee' => $withdraw->charge_fee,
'charge_rate' => $withdraw->charge_rate,
]);
$this->handleLog($withdraw, '用户发起提现申请', $this->user);
return $withdraw;
});
// 检查是否执行自动打款
$autoCheck = false;
if ($type !== 'bank' && $this->config['auto_arrival']) {
$autoCheck = true;
}
if ($autoCheck) {
Db::startTrans();
try {
$withdraw = $this->handleAgree($withdraw, $this->user);
$withdraw = $this->handleWithdraw($withdraw, $this->user);
Db::commit();
} catch (ShoproException $e) {
Db::commit(); // 不回滚,记录错误日志
error_stop($e->getMessage());
} catch (HttpResponseException $e) {
$data = $e->getResponse()->getData();
$message = $data ? ($data['msg'] ?? '') : $e->getMessage();
error_stop($message);
} catch (\Exception $e) {
Db::rollback();
error_stop($e->getMessage());
}
}
return $withdraw;
}
// 同意
public function handleAgree($withdraw, $oper = null)
{
if ($withdraw->status != 0) {
throw new ShoproException('请勿重复操作');
}
$withdraw->status = 1;
$withdraw->save();
return $this->handleLog($withdraw, '同意提现申请', $oper);
}
// 处理打款
public function handleWithdraw($withdraw, $oper = null)
{
$withDrawStatus = false;
if ($withdraw->status != 1) {
throw new ShoproException('请勿重复操作');
}
if ($withdraw->withdraw_type !== 'bank') {
$withDrawStatus = $this->handleTransfer($withdraw);
} else {
$withDrawStatus = true;
}
if ($withDrawStatus) {
$withdraw->status = 2;
$withdraw->paid_fee = $withdraw->amount;
$withdraw->save();
return $this->handleLog($withdraw, '已打款', $oper);
}
return $withdraw;
}
// 拒绝
public function handleRefuse($withdraw, $refuse_msg)
{
if ($withdraw->status != 0 && $withdraw->status != 1) {
throw new ShoproException('请勿重复操作');
}
$withdraw->status = -1;
$withdraw->save();
// 退回用户佣金
WalletService::change($this->user, 'commission', bcadd($withdraw->charge_fee, $withdraw->amount, 2), 'withdraw_error', [
'withdraw_id' => $withdraw->id,
'amount' => $withdraw->amount,
'charge_fee' => $withdraw->charge_fee,
'charge_rate' => $withdraw->charge_rate,
]);
return $this->handleLog($withdraw, '拒绝:' . $refuse_msg);
}
// 企业付款提现
private function handleTransfer($withdraw)
{
operate_disabled();
$type = $withdraw->withdraw_type;
$platform = $withdraw->platform;
$payService = new PayService($type, $platform);
if ($type == 'wechat') {
$payload = [
'out_batch_no' => $withdraw->withdraw_sn,
'batch_name' => '商家转账到零钱',
'batch_remark' => "用户[" . ($withdraw->withdraw_info['微信用户'] ?? '') . "]提现",
'total_amount' => $withdraw->amount,
'total_num' => 1,
'transfer_detail_list' => [
[
'out_detail_no' => $withdraw->withdraw_sn,
'transfer_amount' => $withdraw->amount,
'transfer_remark' => "用户[" . ($withdraw->withdraw_info['微信用户'] ?? '') . "]提现",
'openid' => $withdraw->withdraw_info['微信ID'] ?? '',
'user_name' => $withdraw->withdraw_info['真实姓名'] ?? '',
],
],
];
} elseif ($type == 'alipay') {
$payload = [
'out_biz_no' => $withdraw->withdraw_sn,
'trans_amount' => $withdraw->amount,
'product_code' => 'TRANS_ACCOUNT_NO_PWD',
'biz_scene' => 'DIRECT_TRANSFER',
// 'order_title' => '余额提现到',
'remark' => '用户提现',
'payee_info' => [
'identity' => $withdraw->withdraw_info['支付宝账户'] ?? '',
'identity_type' => 'ALIPAY_LOGON_ID',
'name' => $withdraw->withdraw_info['真实姓名'] ?? '',
]
];
}
try {
list($code, $response) = $payService->transfer($payload);
Log::write('transfer-origin-data' . json_encode($response));
if ($code === 1) {
$withdraw->payment_json = json_encode($response, JSON_UNESCAPED_UNICODE);
$withdraw->save();
return true;
}
throw new ShoproException(json_encode($response, JSON_UNESCAPED_UNICODE));
} catch (HttpResponseException $e) {
$data = $e->getResponse()->getData();
$message = $data ? ($data['msg'] ?? '') : $e->getMessage();
throw new ShoproException($message);
} catch (\Exception $e) {
\think\Log::error('提现失败:' . ' 行号:' . $e->getLine() . '文件:' . $e->getFile() . '错误信息:' . $e->getMessage());
$this->handleLog($withdraw, '提现失败:' . $e->getMessage());
throw new ShoproException($e->getMessage()); // 弹出错误信息
}
return false;
}
private function handleLog($withdraw, $oper_info, $oper = null)
{
$oper = Operator::get($oper);
WithdrawLogModel::insert([
'withdraw_id' => $withdraw->id,
'content' => $oper_info,
'oper_type' => $oper['type'],
'oper_id' => $oper['id'],
'createtime' => time()
]);
return $withdraw;
}
}

View File

@@ -0,0 +1,332 @@
<?php
namespace addons\shopro\service\activity;
use think\Db;
use app\admin\model\shopro\activity\Signin as SigninModel;
use addons\shopro\service\Wallet as WalletService;
use addons\shopro\facade\Activity as ActivityFacade;
class Signin
{
protected $user = null;
protected $activity = null;
protected $rules = [];
public function __construct()
{
$this->user = auth_user();
$this->activity = $this->getSigninActivity();
$rules = $this->activity['rules'];
$this->rules = [
'everyday' => $rules['everyday'] ?? 0,
'is_inc' => $rules['is_inc'] ?? 0,
'inc_num' => $rules['inc_num'] ?? 0,
'until_day' => $rules['until_day'] ?? 0,
'discounts' => $rules['discounts'] ?? [],
'is_replenish' => $rules['is_replenish'] ?? 1,
'replenish_days' => $rules['replenish_days'] ?? 1,
'replenish_limit' => $rules['replenish_limit'] ?? 0,
'replenish_num' => $rules['replenish_num'] ?? 1
];
}
public function getRules()
{
return $this->rules;
}
/**
* 获取签到日历
*
* @param string $month
* @return array
*/
public function getList($month)
{
$signins = SigninModel::where('user_id', $this->user->id)
->where('activity_id', $this->activity['id'])
->where('date', 'like', $month . '%')
->order('date', 'asc')
->select();
$signin_dates = array_column($signins, 'date');
$today = date('Y-m-d');
// 要查询的是否是当前月
$is_current = ($month == date('Y-m')) ? true : false;
// 所选月开始时间戳
$month_start_time = strtotime($month);
// 所选月总天数
$month_days = date('t', $month_start_time);
$days = [];
for ($i = 1; $i <= $month_days; $i++) {
$for_time = $month_start_time + (($i - 1) * 86400);
$for_date = date('Y-m-d', $for_time);
// 如果不是当前月,全是 before, 如果是当前月判断 日期是当前日期的 前面,还是后面
$current = !$is_current ? ($month > date('Y-m') ? 'after' : 'before') : ($for_date == $today ? 'today' : ($for_date < $today ? 'before' : 'after'));
$is_signin = in_array($for_date, $signin_dates); // 是否签到,判断循环的日期,是否在查询的签到记录里面
$days[] = [
'is_sign' => $is_signin ? 1 : 0,
'is_replenish' => ($is_signin || !$this->rules['is_replenish']) ? 0 : ($this->isReplenish($for_date, false) ? 1 : 0),
'date' => $for_date,
'time' => $for_time,
'day' => $i,
'week' => date('w', $for_time),
'current' => $current,
];
}
return $days;
}
/**
* 获取连续签到天数
*
* @param boolean $is_today 是否包含今天
* @return integer
*/
public function getContinueDays($is_today = true)
{
$totime = time();
$continue_days = 0; // 连续签到天数
$chunk = 0; // 第几次 chunk;
$chunk_num = 20; // 每次查 10 条
SigninModel::where('user_id', $this->user->id)
->where('activity_id', $this->activity['id'])
->where('date', '<>', date('Y-m-d')) // 这里不查今天,今天另算
->chunk($chunk_num, function ($signins) use ($totime, &$continue_days, &$chunk, $chunk_num) {
foreach ($signins as $key => $signin) {
$pre_time = $totime - (86400 * (($key + 1) + ($chunk * $chunk_num)));
$pre_date = date('Y-m-d', $pre_time);
if ($signin->date == $pre_date) {
$continue_days++;
} else {
return false;
}
}
$chunk++;
}, 'date', 'desc'); // 如果 date 重复,有坑 (date < 2020-03-28)
if ($is_today) {
$todaySign = SigninModel::where('user_id', $this->user->id)
->where('activity_id', $this->activity['id'])
->where('date', date('Y-m-d'))->find();
if ($todaySign) {
$continue_days++;
}
}
return $continue_days;
}
/**
* 签到
*
* @return \think\Model
*/
public function signin()
{
$signin = Db::transaction(function () {
// 当前时间戳,避免程序执行中间,刚好跨天
$totime = time();
$signin = SigninModel::where('user_id', $this->user->id)
->where('activity_id', $this->activity['id'])
->where('date', date('Y-m-d', $totime))
->lock(true)->find();
if ($signin) {
error_stop('您今天已经签到,明天再来吧');
}
$fullDays = array_column($this->rules['discounts'], null, 'full');
$score = $this->rules['everyday']; // 每日积分基数
// 获取连续签到天数
$continue_days = $this->getContinueDays(false); // 这里查询历史的连续签到天数
$continue_days++; // 算上今天(默认签到)
if ($this->rules['is_inc']) {
// 连续签到天数超出最大连续天数,按照最大连续天数计算
$continue_effec_days = (($continue_days - 1) > $this->rules['until_day']) ? $this->rules['until_day'] : ($continue_days - 1);
// 计算今天应得积分 连续签到两天,第二天所得积分为 $everyday + ((2 - 1) * $inc_value)
$until_add = $continue_effec_days * $this->rules['inc_num']; // 连续签到累加必须大于 0 ,小于 0 舍弃
if ($until_add > 0) { // 避免 until_day 填写小于 等于 0
$score += $until_add;
}
}
if (isset($fullDays[$continue_days])) {
// 今天是连续奖励天数,加上连续签到奖励
$discount = $fullDays[$continue_days];
if (isset($discount['value']) && $discount['value'] > 0) {
$score += $discount['value'];
}
}
// 插入签到记录
$signin = SigninModel::create([
'user_id' => $this->user->id,
'activity_id' => $this->activity['id'],
'date' => date('Y-m-d', $totime),
'score' => $score >= 0 ? $score : 0,
'is_replenish' => 0,
'rules' => $this->rules
]);
// 赠送积分
if ($score > 0) {
WalletService::change($this->user, 'score', $score, 'signin', [
'date' => date('Y-m-d', $totime)
]);
}
return $signin;
});
return $signin;
}
/**
* 补签
*
* @param array $params
* @return \think\Model
*/
public function replenish($params)
{
$signin = Db::transaction(function () use ($params) {
if (!$this->rules['is_replenish']) {
error_stop('当前签到活动不允许补签');
}
$date = $params['date'];
$signin = SigninModel::where('user_id', $this->user->id)
->where('activity_id', $this->activity['id'])
->where('date', $date)
->lock(true)->find();
if ($signin) {
error_stop('您当天已经签到过了,不需要补签');
}
$this->isReplenish($date);
// 补签
$signin = SigninModel::create([
'user_id' => $this->user->id,
'activity_id' => $this->activity['id'],
'date' => $params['date'],
'score' => -$this->rules['replenish_num'],
'is_replenish' => 1,
'rules' => $this->rules
]);
// 扣除补签积分
if ($this->rules['replenish_num'] > 0) {
WalletService::change($this->user, 'score', -$this->rules['replenish_num'], 'replenish_signin', [
'date' => $params['date']
]);
}
return $signin;
});
return $signin;
}
/**
* 判断日期是否可以补签
*
* @param string $date
* @param boolean $is_throw
* @return boolean
*/
private function isReplenish($date, $is_throw = true)
{
$today = date('Y-m-d');
$today_unix = strtotime($today);
$replenish_unix = strtotime($date);
$interval_days = ($today_unix - $replenish_unix) / 86400;
if ($interval_days <= 0) {
return $this->exception('只能补签今天之前的日期', $is_throw);
}
if ($this->rules['replenish_limit'] && $interval_days > $this->rules['replenish_limit']) {
return $this->exception('已经超出最大可补签日期', $is_throw);
}
// 实际签到的天数
$real_days = SigninModel::where('user_id', $this->user->id)
->where('activity_id', $this->activity['id'])
->where('date', '>', $date)
->where('date', '<', $today)
->order('date', 'desc')->count();
// 已补签的天数
$replenish_days = SigninModel::where('user_id', $this->user->id)
->where('activity_id', $this->activity['id'])
->where('date', '>', $date)
->where('date', '<', $today)
->where('is_replenish', 1)
->order('date', 'desc')->count();
$need_days = $interval_days - 1; // 如果时间间隔中没有断签,应该的签到天数
if (($need_days - $real_days) >= ($this->rules['replenish_days'] - $replenish_days)) {
return $this->exception('最多可补签最近' . $this->rules['replenish_days'] . '天,当前所选日期不可补签', $is_throw);
}
return true;
}
/**
* 抛出异常or false
*
* @param string $msg
* @param boolean $is_throw
* @return mixed
*/
public function exception($msg, $is_throw = true)
{
if ($is_throw) {
error_stop($msg);
} else {
return false;
}
}
/**
* 获取签到活动
*
* @return array
*/
private function getSigninActivity()
{
$activities = ActivityFacade::getActivities(['signin'], ['ing']);
$activity = $activities[0] ?? null;
if (!$activity) {
error_stop('签到活动还未开始');
}
return $activity;
}
}

View File

@@ -0,0 +1,541 @@
<?php
namespace addons\shopro\service\commission;
use addons\shopro\service\commission\Config;
use app\admin\model\shopro\user\User as UserModel;
use app\admin\model\shopro\commission\Agent as AgentModel;
use app\admin\model\shopro\commission\Level as LevelModel;
use app\admin\model\shopro\commission\Log as LogModel;
use app\admin\model\shopro\Share as ShareModel;
/**
* 分销商业务
*/
class Agent
{
public $user; // 商城用户
public $agent; // 分销商
public $config; // 分销设置
public $parentUserId;
public $nextAgentTeam;
public $nextUserTeam;
/**
* 构造函数
*
* @param mixed $user 用户ID/用户对象
*/
public function __construct($user)
{
if (is_numeric($user)) {
$this->user = UserModel::get($user);
} else {
$this->user = UserModel::get($user->id);
}
if (!empty($this->user->id)) {
$this->agent = AgentModel::with(['level_info'])->find($this->user->id);
}
$this->config = new Config();
}
/**
* 获取分销商实时状态
*/
public function getAgentStatus($autoCreate = false)
{
if (empty($this->agent)) {
// 自动创建分销商
if ($autoCreate) {
return $this->createNewAgent();
}
return NULL;
}
return $this->agent->status;
}
/**
* 获取分销商可参与状态 正常和冻结都可正常浏览并统计业绩
*/
public function isAgentAvaliable()
{
$status = $this->getAgentStatus();
if (in_array($status, [AgentModel::AGENT_STATUS_NORMAL, AgentModel::AGENT_STATUS_FREEZE])) {
return true;
}
return false;
}
/**
* 获取分销商等级
*/
public function getAgentLevel()
{
if (empty($this->agent)) {
return 0;
}
if (empty($this->agent->level_info)) {
return 1;
}
return $this->agent->level_info->level;
}
/**
* 分销商升级是否锁定
*/
public function getAgentUpgradeLock()
{
if (empty($this->agent)) {
return true;
}
if ($this->agent->upgrade_lock == AgentModel::UPGRADE_LOCK_OPEN) {
return true;
}
return false;
}
/**
* 实时获取上级推荐人
*/
public function getParentUserId()
{
if (empty($this->parentUserId)) {
$this->parentUserId = 0;
$parent_user_id = $this->user->parent_user_id;
// 未直接绑定分销商,从分享记录查找最近的分销商
if ($parent_user_id === NULL) {
$shareLog = ShareModel::hasWhere(
'agent',
function ($query) {
return $query->where('status', 'in', [AgentModel::AGENT_STATUS_NORMAL, AgentModel::AGENT_STATUS_FREEZE]);
}
)->where('Share.user_id', $this->user->id)->order('id desc')->find();
if ($shareLog) {
$parent_user_id = $shareLog['share_id'];
}
}
// 再次检查上级分销商是否可用
if ($parent_user_id > 0) {
$parentUser = UserModel::where('id', $parent_user_id)->find();
$parentAgent = AgentModel::avaliable()->where(['user_id' => $parent_user_id])->find();
if ($parentUser && $parentAgent) {
$this->parentUserId = $parentAgent->user_id;
}
}
}
return $this->parentUserId;
}
/**
* 创建分销商
*/
public function createNewAgent($event = '', $applyInfo = [])
{
// 已经是分销商
if (!empty($this->agent)) {
return $this->getAgentStatus();
}
$agentStatus = AgentModel::AGENT_STATUS_NULL;
$condition = $this->config->getBecomeAgentEvent();
$check = false; // 是否满足条件
$needAgentApplyForm = $this->config->isAgentApplyForm();
if ($event !== '' && $condition['type'] !== $event) {
return $agentStatus;
}
switch ($condition['type']) {
case 'apply': // 直接自助申请
$check = true;
$needAgentApplyForm = true;
break;
case 'goods': // 需购买指定产品
$isBuy = \app\admin\model\shopro\order\Order::hasWhere('items', function ($query) use ($condition) {
return $query->where('goods_id', 'in', $condition['value'])->where('refund_status', 0);
})->where('Order.user_id', $this->user->id)->paid()->find();
if ($isBuy) $check = true;
break;
case 'consume': // 消费累计
if ($this->user->total_consume >= $condition['value']) {
$check = true;
}
break;
case 'user': // 新会员注册
$check = true;
$needAgentApplyForm = false;
break;
}
// 可以成为分销商 检查系统设置
if ($check) {
// 需后台审核
if ($this->config->isAgentCheck()) {
$agentStatus = AgentModel::AGENT_STATUS_PENDING;
} else {
$agentStatus = AgentModel::AGENT_STATUS_NORMAL;
}
// 需要提交资料
if ($needAgentApplyForm && empty($applyInfo)) {
$agentStatus = AgentModel::AGENT_STATUS_NEEDINFO; // 需要主动提交资料,暂时不加分销商信息
}
}
// 可以直接添加分销商信息
if ($agentStatus === AgentModel::AGENT_STATUS_NORMAL || $agentStatus === AgentModel::AGENT_STATUS_PENDING) {
AgentModel::create([
'user_id' => $this->user->id,
'level' => 1, // 默认分销商等级
'status' => $agentStatus,
'apply_info' => $applyInfo,
'apply_num' => 1,
'become_time' => time()
]);
// 绑定上级推荐人
if ($this->user->parent_user_id === NULL) {
if ($this->bindUserRelation('agent') && $agentStatus !== AgentModel::AGENT_STATUS_NORMAL) {
$this->createAsyncAgentUpgrade($this->user->id); // 防止真正成为分销商时重新触发升级任务造成冗余
}
}
// 绑定为平台直推
if ($this->user->parent_user_id === NULL) {
$this->user->parent_user_id = 0;
$this->user->save();
}
$this->agent = AgentModel::with(['level_info'])->find($this->user->id);
// 添加分销商状态记录
LogModel::add($this->user->id, 'agent', ['type' => 'status', 'value' => $agentStatus]);
// 统计分销层级单链业绩
if ($agentStatus === AgentModel::AGENT_STATUS_NORMAL) {
$this->createAsyncAgentUpgrade($this->user->id);
}
}
return $agentStatus;
}
/**
* 绑定用户关系
*
* @param string $event 事件标识(share=点击分享链接, pay=首次支付, agent=成为子分销商)
* @param int $bindAgentId 可指定需绑定的分销商用户ID 默认从分享记录中去查
*/
public function bindUserRelation($event, $bindAgentId = NULL)
{
$bindCheck = false; // 默认不绑定
// 该用户已经有上级
if ($this->user->parent_user_id !== NULL) {
return false;
}
// 不满足绑定下级事件
if ($this->config->getInviteLockEvent() !== $event) {
return false;
}
switch ($this->config->getInviteLockEvent()) {
case 'share':
$bindCheck = true;
break;
case 'pay':
if ($this->user->total_consume > 0) {
$bindCheck = true;
}
break;
case 'agent':
$bindCheck = true;
break;
}
if (!$bindCheck) {
return false;
}
if ($bindAgentId === NULL) {
$bindAgentId = $this->getParentUserId();
}
if (!$bindAgentId) {
return false;
}
$bindAgent = new Agent($bindAgentId);
if (!$bindAgent->isAgentAvaliable()) {
return false;
}
// 允许绑定用户
$this->user->parent_user_id = $bindAgent->user->id;
$this->user->save();
// 添加推广记录
LogModel::add($bindAgent->user->id, 'share', ['user' => $this->user]);
return true;
}
/**
* 创建[分销商升级&统计业绩]异步队列任务
* 为了防止计算量大而引起阻塞,使用异步递归
*/
public function createAsyncAgentUpgrade($user_id = 0)
{
if ($user_id === 0) {
$user_id = $this->user->id;
}
\think\Queue::push('\addons\shopro\job\Commission@agentUpgrade', [
'user_id' => $user_id
], 'shopro');
}
/**
* 执行用户统计、分销商信息统计、分销商等级升级计划 (递归往上升级)
*
* @param bool $upgrade 执行分销商等级升级
*/
public function runAgentUpgradePlan($upgrade = true)
{
if ($this->isAgentAvaliable()) {
// 获取下级直推团队用户信息
$nextUserTeam = $this->getNextUserTeam();
$nextAgentTeam = $this->getNextAgentTeam();
// 一级用户人数
$this->agent->child_user_count_1 = count($nextUserTeam);
// 二级用户人数 = 一级分销商的一级用户人数
$this->agent->child_user_count_2 = array_sum(array_column($nextAgentTeam, 'child_user_count_1'));
// 团队用户人数 = 一级用户人数 + 一级用户的团队用户人数
$this->agent->child_user_count_all = $this->agent->child_user_count_1 + array_sum(array_column($nextAgentTeam, 'child_user_count_all'));
// 一级分销商人数
$this->agent->child_agent_count_1 = count($nextAgentTeam);
// 二级分销商人数 = 一级分销商的一级分销商人数
$this->agent->child_agent_count_2 = array_sum(array_column($nextAgentTeam, 'child_agent_count_1'));
// 团队分销商人数 = 一级分销商人数 + 一级分销商的团队分销商人数
$this->agent->child_agent_count_all = $this->agent->child_agent_count_1 + array_sum(array_column($nextAgentTeam, 'child_agent_count_all'));
// 二级分销订单金额 = 一级分销商的一级分销订单金额 + 一级分销商的自购订单金额
$this->agent->child_order_money_2 = array_sum(array_column($nextAgentTeam, 'child_order_money_1')) + array_sum(array_column($nextAgentTeam, 'child_order_money_0'));
// 团队分销订单金额 = 自购分销订单金额 + 一级分销订单金额 + 一级所有分销商的团队分销订单总金额
$this->agent->child_order_money_all = $this->agent->child_order_money_0 + $this->agent->child_order_money_1 + array_sum(array_column($nextAgentTeam, 'child_order_money_all'));
// 二级分销订单数量 = 一级分销商的一级分销订单数量 + 一级分销商的自购订单数量
$this->agent->child_order_count_2 = array_sum(array_column($nextAgentTeam, 'child_order_count_1')) + array_sum(array_column($nextAgentTeam, 'child_order_count_0'));
// 团队分销订单数量 = 自购分销订单数量 + 一级分销订单数量 + 一级所有分销商的团队分销订单总数量
$this->agent->child_order_count_all = $this->agent->child_order_count_0 + $this->agent->child_order_count_1 + array_sum(array_column($nextAgentTeam, 'child_order_count_all'));
// 一级分销商等级统计
$child_agent_level_1 = array_count_values(array_column($nextAgentTeam, 'level'));
ksort($child_agent_level_1);
$this->agent->child_agent_level_1 = $child_agent_level_1;
// 团队分销商等级统计 = 一级分销商等级 + 一级分销商的团队分销商等级
$child_agent_level_all = $this->childAgentLevelCount(array_column($nextAgentTeam, 'child_agent_level_all'), $this->agent->child_agent_level_1);
ksort($child_agent_level_all);
$this->agent->child_agent_level_all = $child_agent_level_all;
$this->agent->save();
// 分销商自动升级
if (!$this->getAgentUpgradeLock() && $upgrade) {
$canUpgradeLevel = $this->checkAgentUpgradeLevel();
if ($canUpgradeLevel) {
if ($this->config->isUpgradeCheck()) {
$this->agent->level_status = $canUpgradeLevel;
} else {
$this->agent->level = $canUpgradeLevel;
LogModel::add($this->user->id, 'agent', ['type' => 'level', 'level' => LevelModel::find($canUpgradeLevel)]);
}
$this->agent->save();
}
}
\think\Log::info('统计分销商业绩[ID=' . $this->user->id . '&nickname=' . $this->user->nickname . '] ---Ended');
}
$parentUserId = $this->getParentUserId();
if ($parentUserId) {
$this->createAsyncAgentUpgrade($parentUserId);
}
}
/**
* 统计团队分销商等级排布
*/
private function childAgentLevelCount($childAgentLevelArray, $childAgentLevel1Array)
{
$childAgentLevelCount = [];
foreach ($childAgentLevelArray as &$agentLevel) {
if (!empty($agentLevel)) {
$agentLevel = json_decode($agentLevel, true);
array_walk($agentLevel, function ($count, $level) use (&$childAgentLevelCount) {
if (isset($childAgentLevelCount[$level])) {
$childAgentLevelCount[$level] += $count;
} else {
$childAgentLevelCount[$level] = $count;
}
});
}
}
array_walk($childAgentLevel1Array, function ($count, $level) use (&$childAgentLevelCount) {
if (isset($childAgentLevelCount[$level])) {
$childAgentLevelCount[$level] += $count;
} else {
$childAgentLevelCount[$level] = $count;
}
});
return $childAgentLevelCount;
}
/**
* 获取下级分销商团队
*/
public function getNextAgentTeam()
{
if (!$this->isAgentAvaliable()) {
return [];
}
if (empty($this->nextAgentTeam)) {
$this->nextAgentTeam = AgentModel::hasWhere('user', function ($query) {
return $query->where('parent_user_id', $this->user->id);
})->column('user_id, Agent.level, child_user_count_1, child_user_count_all,child_agent_count_1, child_agent_count_all, child_order_money_0, child_order_money_1, child_order_money_all, child_order_count_0, child_order_count_1, child_order_count_all, child_agent_level_1, child_agent_level_all');
}
return $this->nextAgentTeam;
}
/**
* 获取下级直推团队用户
*/
public function getNextUserTeam()
{
if (!$this->isAgentAvaliable()) {
return [];
}
if (empty($this->nextUserTeam)) {
$this->nextUserTeam = UserModel::where(['parent_user_id' => $this->user->id, 'status' => 'normal'])->column('id');
}
return $this->nextUserTeam;
}
/**
* 获取可升级的分销商等级
*/
private function getNextAgentLevel()
{
$nextAgentLevel = [];
$agentLevel = $this->getAgentLevel();
if ($agentLevel) {
$nextAgentLevel = LevelModel::where('level', '>', $agentLevel)->order('level asc')->select();
}
return $nextAgentLevel;
}
/**
* 比对当前分销商条件是否满足升级规则
*/
private function checkAgentUpgradeLevel()
{
$nextAgentLevel = $this->getNextAgentLevel();
if (count($nextAgentLevel)) {
foreach ($nextAgentLevel as $level) {
$checkLevel[$level->level] = $this->isMatchUpgradeLevelRule($level);
// 不允许越级升级
if (!$this->config->isUpgradeJump()) break;
}
$checkLevel = array_reverse($checkLevel, true);
$canUpgradeLevel = array_search(true, $checkLevel);
if ($canUpgradeLevel) {
return $canUpgradeLevel;
}
}
return 0;
}
/**
* 分销商升级规则检查
*/
public function isMatchUpgradeLevelRule($level)
{
foreach ($level->upgrade_rules as $name => $value) {
$match[$name] = false;
switch ($name) {
case 'total_consume': // 用户消费金额
$match[$name] = $this->user->$name >= $value;
break;
case 'child_user_count_all': // 团队用户人数
case 'child_user_count_1': // 一级用户人数
case 'child_user_count_2': // 二级用户人数
case 'child_order_money_0': // 自购分销订单金额
case 'child_order_money_1': // 一级分销订单金额
case 'child_order_money_2': // 二级分销订单金额
case 'child_order_money_all': // 团队分销订单金额
case 'child_order_count_0': // 自购分销订单数量
case 'child_order_count_1': // 一级分销订单数量
case 'child_order_count_2': // 二级分销订单数量
case 'child_order_count_all': // 团队分销订单数量
case 'child_agent_count_1': // 一级分销商人数
case 'child_agent_count_2': // 二级分销商人数
case 'child_agent_count_all': // 团队分销商人数
$match[$name] = $this->agent->$name >= $value;
break;
case 'child_agent_level_1': // 一级分销商等级统计
case 'child_agent_level_all': // 团队分销商等级统计
$match[$name] = true;
if (count($value) > 0) {
if (empty($this->agent->$name)) {
$match[$name] = false;
} else {
foreach ($value as $k => $row) {
if (!isset(($this->agent->$name)[$row['level']]) || ($this->agent->$name)[$row['level']] < $row['count']) {
$match[$name] = false;
break;
}
}
}
}
break;
}
// ①满足任意一种条件:只要有一种符合立即返回可以升级状态
if (!$level->upgrade_type && $match[$name]) {
return true;
break;
}
// ②满足所有条件:不满足任意一种条件立即返回不可升级状态
if ($level->upgrade_type && !$match[$name]) {
return false;
break;
}
}
// 循环完所有的 如果是①的情况则代表都不符合条件,如果是②则代表都符合条件 返回对应状态即可
return boolval($level->upgrade_type);
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace addons\shopro\service\commission;
class Config
{
protected $config;
public function __construct()
{
$this->config = sheep_config('shop.commission');
}
// 覆盖默认分销设置
public function setConfig($config)
{
foreach ($config as $name => $value) {
$this->config[$name] = $value;
}
}
// 获取绑定推广关系的事件节点
public function getInviteLockEvent()
{
$becomeAgentEvent = $this->getBecomeAgentEvent();
if($becomeAgentEvent === 'user') {
return 'agent';
}
return $this->config['invite_lock'];
}
// 获取成为分销商的条件
public function getBecomeAgentEvent()
{
return $this->config['become_agent'];
}
// 分销商是否需要审核
public function isAgentCheck()
{
return boolval($this->config['agent_check']);
}
// 分销商升级审核
public function isUpgradeCheck()
{
return boolval($this->config['upgrade_check']);
}
// 分销商允许越级升级
public function isUpgradeJump()
{
return boolval($this->config['upgrade_jump']);
}
// 是否需要完善分销商表单信息
public function isAgentApplyForm()
{
return boolval($this->config['agent_form']['status']);
}
// 获取申请资料表单信息
public function getAgentForm()
{
return $this->config['agent_form'];
}
// 申请协议
public function getApplyProtocol()
{
return $this->config['apply_protocol'];
}
// 分销层级
public function getCommissionLevel()
{
return intval($this->config['level']);
}
// 是否允许分销自购
public function isSelfBuy()
{
return boolval($this->config['self_buy']);
}
// 是否显示升级条件
public function isUpgradeDisplay()
{
return boolval($this->config['upgrade_display']);
}
// 佣金结算价格类型 pay_price=支付金额 goods_price=商品价格 (都不含运费 因为运费对于平台和用户没有实际价值)
public function getRewardType()
{
return $this->config['reward_type'];
}
// 佣金结算节点 payed=支付后
public function getRewardEvent()
{
return $this->config['reward_event'];
}
// 退款是否扣除分销业绩
public function getRefundCommissionOrder()
{
return boolval($this->config['refund_commission_order']);
}
// 退款是否扣除佣金
public function getRefundCommissionReward()
{
return boolval($this->config['refund_commission_reward']);
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace addons\shopro\service\commission;
use app\admin\model\shopro\commission\Level as LevelModel;
use app\admin\model\shopro\commission\CommissionGoods as CommissionGoodsModel;
/**
* 分销商品
*/
class Goods
{
public $commissionGoods; // 分销商品
protected $commissionConfig = NULL; // 独立分销设置 默认无
protected $commissionRules; // 分销规则
protected $skuPriceId; // 商品规格ID
/**
* 获取商品实时分佣规则
*
* @param int $goods 商品
* @param int $skuId 商品规格ID
*/
public function __construct($goods, $skuPriceId = 0)
{
$commissionRules = CommissionGoodsModel::GOODS_COMMISSION_STATUS_OFF;
if(is_numeric($goods)) {
$this->commissionGoods = CommissionGoodsModel::where(['goods_id' => $goods, 'status' => CommissionGoodsModel::GOODS_COMMISSION_STATUS_ON])->find();
}else {
$this->commissionGoods = $goods;
}
$this->skuPriceId = $skuPriceId;
if ($this->commissionGoods) {
$commission_config = $this->commissionGoods->commission_config;
if ($commission_config['status']) {
$this->commissionConfig = $commission_config;
}
switch ($this->commissionGoods->self_rules) {
// 默认分销规则
case CommissionGoodsModel::GOODS_COMMISSION_RULES_DEFAULT:
$commissionRules = $this->getDefaultCommissionRules();
break;
// 独立分销规则
case CommissionGoodsModel::GOODS_COMMISSION_RULES_SELF:
$commissionRules = $this->getSelfCommissionRules();
break;
// 批量分销规则
case CommissionGoodsModel::GOODS_COMMISSION_RULES_BATCH:
$commissionRules = $this->getBatchCommissionRules();
break;
}
}
$this->commissionRules = $commissionRules;
}
public function getCommissionConfig()
{
return $this->commissionConfig;
}
public function getCommissionRules()
{
return $this->commissionRules;
}
/**
* 获取对应分销商等级、对应层级的商品佣金规则
*
* @param int $agentLevel 分销商等级(不是id)
* @param int $commissionLevel 分销商层级(默认一级)
*/
public function getCommissionLevelRule($agentLevel, $commissionLevel = 1)
{
if (isset($this->commissionRules[$agentLevel]) && isset($this->commissionRules[$agentLevel][$commissionLevel])) {
$commissionRule = $this->commissionRules[$agentLevel][$commissionLevel];
return $commissionRule;
}
return false;
}
/**
* 计算对应规则分销佣金
*
* @param int $commissionRule 分销规则
* @param int $amount 结算价格
* @param int $goodsNum 购买数量
*/
public function caculateGoodsCommission($commissionRule, $amount, $goodsNum = 1)
{
$commission = 0;
if (!empty($commissionRule['rate']) && $commissionRule['rate'] > 0) {
$commission = round($amount * $commissionRule['rate'] * 0.01, 2);
}
if (!empty($commissionRule['money']) && $commissionRule['money'] > 0) {
$commission = $commissionRule['money'] * $goodsNum;
}
return number_format($commission, 2, '.', '');
}
// 获取分销商等级默认规则
private function getDefaultCommissionRules()
{
$agentLevelRules = LevelModel::order('level asc')->column('commission_rules', 'level');
$commissionRules = [];
foreach ($agentLevelRules as $agentLevel => $rule) {
$rule = json_decode($rule, true);
foreach ($rule as $commission_level => $percent) {
$commission_level = explode('_', $commission_level);
$level = intval($commission_level[1]);
$commissionRules[$agentLevel][$level] = ['rate' => $percent];
}
}
return $commissionRules;
}
// 获取分销商品独立分佣规则
private function getSelfCommissionRules()
{
$commissionRules = [];
if (isset($this->commissionGoods->commission_rules[$this->skuPriceId])) {
$commissionRules = $this->commissionGoods->commission_rules[$this->skuPriceId];
}
return $commissionRules;
}
// 获取商品批量分佣规则
private function getBatchCommissionRules()
{
return $this->commissionGoods->commission_rules;
}
}

View File

@@ -0,0 +1,235 @@
<?php
namespace addons\shopro\service\commission;
use addons\shopro\service\commission\Config;
use app\admin\model\shopro\commission\Order as OrderModel;
use app\admin\model\shopro\commission\Reward as RewardModel;
use app\admin\model\shopro\commission\Log as LogModel;
/**
* 分佣业务
*/
class Order
{
public $config; // 分销设置
public $item; // 订单商品
public $buyer; // 购买人
public $goods; // 分销商品
public $amount = 0; // 订单商品核算价格
public $commissionLevel = 0; // 分销层级
public $selfBuy; // 是否自购
// 分销状态
const COMMISSION_CLOSE = 0; // 分销功能已关闭
/**
* 构造函数
*
* @param array $item 分销商品
*/
public function __construct($item)
{
$this->buyer = new Agent($item['user_id']);
$this->item = $item;
$this->config = new Config();
}
/**
* 检测并设置分销商品规则
*
* @return void
*/
public function checkAndSetCommission()
{
// 未找到订单商品或购买人
if (!$this->item || !$this->buyer->user) {
return false;
}
// 获取商品分销佣金规则
$this->goods = new Goods($this->item['goods_id'], $this->item['goods_sku_price_id']);
// 商品有独立分销设置,覆盖默认系统配置
if ($commissionConfig = $this->goods->getCommissionConfig()) {
$this->config->setConfig($commissionConfig);
}
// 获取系统设置分销层级
$this->commissionLevel = $this->config->getCommissionLevel();
// 分销中心已关闭
if (self::COMMISSION_CLOSE === $this->commissionLevel) {
return false;
}
// 商品不参与分销
if (!$this->goods->getCommissionRules()) {
return false;
}
// 是否自购分销订单
$this->selfBuy = $this->buyer->isAgentAvaliable() && $this->config->isSelfBuy();
// 未找到上级分销商且不是自购
if (!$this->buyer->getParentUserId() && !$this->selfBuy) {
return false;
}
// 获取商品结算价格(四舍五入精确到分)
$this->amount = $this->getGoodsCommissionAmount();
// 没有分佣的必要了
if ($this->amount <= 0) {
return false;
}
return true;
}
// 获取商品核算总金额
public function getGoodsCommissionAmount()
{
$commissionType = $this->config->getRewardType();
$amount = round(0, 2);
switch ($commissionType) {
case 'pay_price':
$amount = $this->item['pay_fee'];
break;
case 'goods_price':
$amount = round($this->item['goods_price'] * $this->item['goods_num'], 2);
break;
}
return $amount;
}
/**
* 创建分销订单
*/
public function createCommissionOrder()
{
$agentId = $this->selfBuy ? $this->buyer->user->id : $this->buyer->getParentUserId();
if (!$agentId) {
return false;
}
$commissionOrder = OrderModel::where('order_item_id', $this->item['id'])->find();
// 已添加过分销订单
if ($commissionOrder) {
return $commissionOrder;
}
$commissionOrderParams = [
'self_buy' => intval($this->selfBuy),
'order_id' => $this->item['order_id'],
'order_item_id' => $this->item['id'],
'buyer_id' => $this->buyer->user->id,
'goods_id' => $this->item['goods_id'],
'agent_id' => $agentId,
'amount' => $this->amount,
'reward_type' => $this->config->getRewardType(),
'commission_rules' => $this->goods->getCommissionRules(), // 记录当前设置的分佣规则,防止以后系统或者分销商设置有变导致更改历史规则
'reward_event' => $this->config->getRewardEvent(),
'commission_order_status' => $this->goods->commissionGoods->commission_order_status, // 是否计入分销业绩
'commission_reward_status' => RewardModel::COMMISSION_REWARD_STATUS_PENDING, // 佣金状态
];
$commissionOrder = OrderModel::create($commissionOrderParams);
// 添加分销商推广订单业绩
$orderAgent = new Agent($commissionOrder->agent_id);
if ($orderAgent->isAgentAvaliable() && $commissionOrder->commission_order_status) {
if($this->selfBuy) {
// 添加自购业绩
$orderAgent->agent->setInc('child_order_money_0', $commissionOrder->amount);
$orderAgent->agent->setInc('child_order_count_0', 1);
}else {
// 添加一级业绩
$orderAgent->agent->setInc('child_order_money_1', $commissionOrder->amount);
$orderAgent->agent->setInc('child_order_count_1', 1);
}
LogModel::add($commissionOrder['agent_id'], 'order', [
'type' => 'paid',
'order' => $commissionOrder,
'item' => $this->item,
'buyer' => $this->buyer->user
], $this->buyer->user);
}
return $commissionOrder;
}
/**
* 执行分销计划,递归往上分佣
*
* @param object $commissionOrder 分销订单
* @param object $currentAgent 当前待分佣的分销商 默认自购开始算
* @param int $currentCommissionLevel 当前分佣层级 一级(自购)开始算
*/
public function runCommissionPlan($commissionOrder, $currentAgent = null, $currentCommissionLevel = 1)
{
// 超出分佣层级
if ($this->commissionLevel < $currentCommissionLevel) {
return true;
}
// 当前层级为1且分销订单是自购订单 则当前分销商为购买人自己
if ($currentCommissionLevel === 1) {
$currentAgent = new Agent($commissionOrder->agent_id);
}
if ($currentAgent && !empty($currentAgent->user)) {
// 防止重复添加佣金
$commissionReward = RewardModel::where([
'commission_order_id' => $commissionOrder->id,
'agent_id' => $currentAgent->user->id,
'commission_level' => $currentCommissionLevel, // 分佣层级
])->find();
if (!$commissionReward) {
$currentAgentLevel = $currentAgent->getAgentLevel();
$currentCommissionLevelRule = $this->goods->getCommissionLevelRule($currentAgentLevel, $currentCommissionLevel);
if ($currentCommissionLevelRule) {
$commission = $this->goods->caculateGoodsCommission($currentCommissionLevelRule, $this->amount, $this->item['goods_num']);
if ($commission > 0) {
$commissionRewardParams = [
'order_id' => $commissionOrder->order_id,
'order_item_id' => $commissionOrder->order_item_id,
'buyer_id' => $commissionOrder->buyer_id, // 购买人
'commission_order_id' => $commissionOrder->id, // 分销订单ID
'agent_id' => $currentAgent->user->id, // 待分佣分销商ID
'type' => 'commission', // 奖金类型
'commission' => $commission, // 佣金
'status' => 0, // 佣金状态
'original_commission' => $commission, // 原始佣金
'commission_level' => $currentCommissionLevel, // 分佣层级
'commission_rules' => $currentCommissionLevelRule, // 分佣层级
'agent_level' => $currentAgentLevel // 分佣时分销商等级
];
$commissionReward = RewardModel::create($commissionRewardParams);
LogModel::add($commissionReward['agent_id'], 'reward', [
'type' => 'paid',
'reward' => $commissionReward,
], $this->buyer->user);
}
}
}
// 递归执行下一次
$currentCommissionLevel++;
// 超出分销层级
if ($this->commissionLevel < $currentCommissionLevel) {
return true;
}
$parentUserId = $currentAgent->getParentUserId();
// 执行下一级分销任务
if ($parentUserId) {
$parentAgent = new Agent($parentUserId);
$this->runCommissionPlan($commissionOrder, $parentAgent, $currentCommissionLevel);
} else {
return true;
}
}
}
}

View File

@@ -0,0 +1,274 @@
<?php
namespace addons\shopro\service\commission;
// use addons\shopro\library\Oper;
use app\admin\model\shopro\commission\Log as LogModel;
use app\admin\model\shopro\commission\Order as OrderModel;
use app\admin\model\shopro\commission\Reward as RewardModel;
use app\admin\model\shopro\user\User as UserModel;
use addons\shopro\service\Wallet as WalletService;
/**
* 结算奖金
*/
class Reward
{
protected $event = '';
/**
* 执行奖金计划,直接派发佣金
*
* @param string $event 分佣的事件
*/
public function __construct($event)
{
$this->event = $event;
}
/**
* 执行奖金计划, 派发整单佣金
*
* @param mixed $commissionOrder|$commissionOrderId 分销订单
*/
public function runCommissionRewardByOrder($commissionOrder)
{
if (is_numeric($commissionOrder)) {
$commissionOrder = OrderModel::find($commissionOrder);
}
// 未找到分销订单
if (!$commissionOrder) {
return false;
}
// 已经操作过了
if ($commissionOrder['commission_reward_status'] !== RewardModel::COMMISSION_REWARD_STATUS_PENDING) {
return false;
}
$rewardEvent = $commissionOrder['reward_event'];
// 不满足分佣事件
if ($rewardEvent !== $this->event && $this->event !== 'admin') {
return false;
}
// 更新分销订单结算状态
$commissionOrder->commission_reward_status = RewardModel::COMMISSION_REWARD_STATUS_ACCOUNTED;
$commissionOrder->commission_time = time();
$commissionOrder->save();
// 防止重复添加佣金
$commissionRewards = RewardModel::where([
'commission_order_id' => $commissionOrder['id'],
'status' => RewardModel::COMMISSION_REWARD_STATUS_PENDING,
])->select();
// 添加分销商收益、余额添加钱包、更新分销佣金结算状态、提醒分销商到账
if (count($commissionRewards) > 0) {
foreach ($commissionRewards as $commissionReward) {
$this->runCommissionReward($commissionReward);
}
}
return true;
}
/**
* 执行奖金计划,直接派发佣金
*
* @param mixed $commissionReward|$commissionRewardId 奖金记录
*/
public function runCommissionReward($commissionReward)
{
if (is_numeric($commissionReward)) {
$commissionReward = RewardModel::find($commissionReward);
}
// 未找到奖金记录
if (!$commissionReward) {
return false;
}
if ($commissionReward->status == RewardModel::COMMISSION_REWARD_STATUS_PENDING) {
$rewardAgent = new Agent($commissionReward->agent_id);
if ($rewardAgent->isAgentAvaliable()) {
$rewardAgent->agent->setInc('total_income', $commissionReward->commission);
$commissionReward->status = RewardModel::COMMISSION_REWARD_STATUS_ACCOUNTED;
$commissionReward->commission_time = time();
$commissionReward->save();
WalletService::change($rewardAgent->user, $commissionReward->type, $commissionReward->commission, 'reward_income', $commissionReward);
LogModel::add($rewardAgent->user->id, 'reward', [
'type' => $this->event,
'reward' => $commissionReward
]);
}
}
return true;
}
/**
* 扣除/取消 分销订单
*
* @param mixed $commissionOrder|$commissionOrderId 分销订单
* @param array $deductOrderMoney 扣除分销商的订单业绩 默认扣除
* @param array $deleteReward 扣除分销订单奖金 默认扣除
*/
public function backCommissionRewardByOrder($commissionOrder, $deductOrderMoney = true, $deleteReward = true)
{
if ($this->event !== 'admin' && $this->event !== 'refund') {
return false;
}
if ($this->event === 'refund') {
$config = new Config();
$deductOrderMoney = $config->getRefundCommissionOrder();
$deleteReward = $config->getRefundCommissionReward();
}
if (!$deductOrderMoney && !$deleteReward) {
return false;
}
if (is_numeric($commissionOrder)) {
$commissionOrder = OrderModel::find($commissionOrder);
}
// 未找到分销订单
if (!$commissionOrder) {
return false;
}
// 扣除分销商的订单业绩
if ($deductOrderMoney) {
// 变更分销订单状态
if ($commissionOrder->commission_order_status == OrderModel::COMMISSION_ORDER_STATUS_YES) { // 扣除
$commissionOrder->commission_order_status = OrderModel::COMMISSION_ORDER_STATUS_BACK;
$commissionOrder->save();
$orderAgent = new Agent($commissionOrder->agent_id);
// 扣除分销订单业绩
if($commissionOrder->self_buy) {
$orderAgent->agent->setDec('child_order_money_0', $commissionOrder->amount);
$orderAgent->agent->setDec('child_order_count_0', 1);
}else {
$orderAgent->agent->setDec('child_order_money_1', $commissionOrder->amount);
$orderAgent->agent->setDec('child_order_count_1', 1);
}
// 重新计算分销链条业绩
$orderAgent->createAsyncAgentUpgrade();
LogModel::add($orderAgent->user->id, 'order', [
'type' => $this->event,
'order' => $commissionOrder,
'buyer' => UserModel::find($commissionOrder->buyer_id)
]);
}
if ($commissionOrder->commission_order_status == OrderModel::COMMISSION_ORDER_STATUS_NO) { // 取消
$commissionOrder->commission_order_status = OrderModel::COMMISSION_ORDER_STATUS_CANCEL;
$commissionOrder->save();
}
}
// 变更分销订单佣金执行状态
if ($deleteReward) {
if ($commissionOrder->commission_reward_status == RewardModel::COMMISSION_REWARD_STATUS_ACCOUNTED) { // 扣除
$commissionOrder->commission_reward_status = RewardModel::COMMISSION_REWARD_STATUS_BACK;
$commissionOrder->save();
// 防止重复扣除佣金
$commissionRewards = RewardModel::where([
'commission_order_id' => $commissionOrder->id,
'status' => RewardModel::COMMISSION_REWARD_STATUS_ACCOUNTED,
])->select();
if (count($commissionRewards) > 0) {
// 扣除分销佣金
foreach ($commissionRewards as $commissionReward) {
$this->backCommissionReward($commissionReward);
}
}
}
}
if ($commissionOrder->commission_reward_status == RewardModel::COMMISSION_REWARD_STATUS_PENDING) { // 取消
$commissionOrder->commission_reward_status = RewardModel::COMMISSION_REWARD_STATUS_CANCEL;
$commissionOrder->save();
$commissionRewards = RewardModel::where([
'commission_order_id' => $commissionOrder->id,
'status' => RewardModel::COMMISSION_REWARD_STATUS_PENDING
])->select();
// 取消分销佣金
if (count($commissionRewards) > 0) {
foreach ($commissionRewards as $commissionReward) {
$this->cancelCommissionReward($commissionReward);
}
}
}
return true;
}
/**
* 扣除单笔分销佣金
*
* @param mixed $commissionReward|$commissionRewardId 奖金记录
*/
public function backCommissionReward($commissionReward)
{
if (is_numeric($commissionReward)) {
$commissionReward = RewardModel::find($commissionReward);
}
// 未找到奖金记录
if (!$commissionReward) {
return false;
}
if ($commissionReward->status == RewardModel::COMMISSION_REWARD_STATUS_ACCOUNTED) {
$rewardAgent = new Agent($commissionReward->agent_id);
if ($rewardAgent->agent->total_income < $commissionReward->commission) {
$rewardAgent->agent->total_income = 0;
$rewardAgent->agent->save();
} else {
$rewardAgent->agent->setDec('total_income', $commissionReward->commission);
}
WalletService::change($rewardAgent->user, $commissionReward->type, -$commissionReward->commission, 'reward_back', $commissionReward);
$commissionReward->status = RewardModel::COMMISSION_REWARD_STATUS_BACK;
$commissionReward->save();
LogModel::add($rewardAgent->user->id, 'reward', [
'type' => $this->event,
'reward' => $commissionReward,
]);
}
return true;
}
/**
* 取消单笔分销佣金
*
* @param mixed $commissionReward|$commissionRewardId 奖金记录
*/
public function cancelCommissionReward($commissionReward)
{
if (is_numeric($commissionReward)) {
$commissionReward = RewardModel::find($commissionReward);
}
// 未找到奖金记录
if (!$commissionReward) {
return false;
}
if ($commissionReward->status == RewardModel::COMMISSION_REWARD_STATUS_PENDING) {
$commissionReward->status = RewardModel::COMMISSION_REWARD_STATUS_CANCEL;
$commissionReward->save();
$rewardAgent = new Agent($commissionReward->agent_id);
LogModel::add($rewardAgent->user->id, 'reward', [
'type' => $this->event,
'reward' => $commissionReward,
]);
}
return true;
}
}

View File

@@ -0,0 +1,444 @@
<?php
namespace addons\shopro\service\goods;
use app\admin\model\shopro\goods\Goods;
use app\admin\model\shopro\goods\SkuPrice;
use app\admin\model\shopro\app\ScoreSkuPrice;
use app\admin\model\shopro\Category;
use think\Db;
use addons\shopro\library\Tree;
use addons\shopro\service\SearchHistory;
use addons\shopro\facade\Activity as ActivityFacade;
class GoodsService
{
protected $query = null;
protected $order = [];
protected $md5s = [];
protected $format = null;
protected $is_activity = false; // 是否处理活动
protected $show_score_shop = false; // 查询积分商城商品
public function __construct(\Closure $format = null, \think\db\Query $query = null)
{
$this->query = $query ?: new Goods();
$this->format = $format;
}
/**
* with 关联
*
* @param mixed $with
* @return self
*/
public function with($with)
{
$this->query->with($with);
return $this;
}
/**
* 查询上架的商品,包含隐藏商品
*
* @return self
*/
public function show()
{
$this->md5s[] = 'show';
$this->query = $this->query->whereIn('status', ['up', 'hidden']);
return $this;
}
/**
* 查询上架的商品,不包含隐藏商品
*
* @return self
*/
public function up()
{
$this->md5s[] = 'up';
$this->query = $this->query->where('status', 'up');
return $this;
}
/**
* 是否要处理活动相关数据
*
* @return self
*/
public function activity($activity = false)
{
$this->is_activity = $activity ? true : false;
return $this;
}
/**
* 查询积分商城商品
*
* @return self
*/
public function score()
{
$this->show_score_shop = true;
// 获取所有积分商品
$scoreGoodsIds = ScoreSkuPrice::where('status', 'up')->group('goods_id')->cache(60)->column('goods_id');
$this->query = $this->query->whereIn('id', $scoreGoodsIds);
return $this;
}
/**
* 连表查询 库存,暂时没用
*
* @return self
*/
public function stock()
{
$this->md5s[] = 'stock';
$skuSql = SkuPrice::field('sum(stock) as stock, goods_id as sku_goods_id')->group('goods_id')->buildSql();
$this->query = $this->query->join([$skuSql => 'sp'], 'id = sp.sku_goods_id', 'left');
return $this;
}
/**
* 搜索商品
*
* @return self
*/
public function search($keyword)
{
$keyword = is_string($keyword) ? $keyword : json_encode($keyword); // 转字符串
$this->md5s[] = 'search:' . $keyword; // 待补充
// 添加搜索记录
$searchHistory = new SearchHistory();
$searchHistory->save(['keyword' => $keyword]);
$this->query = $this->query->where('title|subtitle', 'like', '%' . $keyword . '%');
return $this;
}
/**
* 根据 ids 获取商品
*
* @param string|array $ids ids 数组或者 , 隔开的字符串
* @return self
*/
public function whereIds($ids = '')
{
$ids = array_map('intval', is_array($ids) ? $ids : explode(',', $ids));
$this->md5s[] = 'ids:' . implode(',', $ids);
if (!empty($ids)) {
$this->query = $this->query->orderRaw('field(id, ' . implode(',', $ids) . ')'); // 按照 ids 里面的 id 进行排序
$this->query = $this->query->whereIn('id', $ids);
}
return $this;
}
/**
* 根据商品分类获取商品
*
* @param integer $category_id
* @return self
*/
public function category($category_id, $is_category_deep = true)
{
$category_id = intval($category_id);
$this->md5s[] = 'category_id:' . $category_id;
$category_ids = [];
if (isset($category_id) && $category_id != 0) {
if ($is_category_deep) {
// 查询分类所有子分类,包括自己
$category_ids = (new Tree(function () {
// 组装搜索条件,排序等
return (new Category)->normal();
}))->getChildIds($category_id);
} else {
$category_ids = [$category_id];
}
}
$this->query->where(function ($query) use ($category_ids) {
// 所有子分类使用 find_in_set or 匹配,亲测速度并不慢
foreach ($category_ids as $key => $category_id) {
$query->whereOrRaw("find_in_set($category_id, category_ids)");
}
});
return $this;
}
/**
* 排序,方法参数同 thinkphp order 方法
*
* @param string $sort
* @param string $order
* @return self
*/
public function order($sort = 'weigh', $order = 'desc')
{
$sort = $sort == 'price' ? Db::raw('convert(`price`, DECIMAL(10, 2)) ' . $order) : $sort;
$this->query->order($sort, $order);
return $this;
}
/**
* 获取商品列表
*
* @param bool $is_cache 是否缓存
* @return array|\think\model\Collection
*/
public function select($is_cache = false)
{
$this->md5s[] = 'select';
// 默认排序
$this->order();
$goods = $this->query->field('*,(sales + show_sales) as total_sales')
->cache($this->getCacheKey($is_cache), (200 + mt_rand(0, 100)))
->select();
// 格式化数据
foreach ($goods as $key => $gd) {
$gd = $this->defaultFormat($gd);
if ($this->format instanceof \Closure) {
$gd = ($this->format)($gd, $this);
}
$goods[$key] = $gd;
}
return $goods;
}
/**
* 获取商品列表
*
* @param bool $is_cache 是否缓存
* @return \think\Paginator
*/
public function paginate($is_cache = false)
{
$this->md5s[] = 'paginate';
// 默认排序
$this->order();
$goods = $this->query->field('*,(sales + show_sales) as total_sales')
->cache($this->getCacheKey($is_cache), (200 + mt_rand(0, 100)))
->paginate(request()->param('list_rows', 10));
// 格式化数据
$goods->each(function($god) {
$god = $this->defaultFormat($god);
if ($this->format instanceof \Closure) {
($this->format)($god, $this);
}
});
return $goods;
}
/**
* 获取单个商品
*
* @param bool $is_cache
* @return Goods
*/
public function find($is_cache = false)
{
$this->md5s[] = 'find';
$goods = $this->query->cache($this->getCacheKey($is_cache), (200 + mt_rand(0, 100)))->find();
if ($goods && $this->format instanceof \Closure) {
// 格式化数据
$goods = $this->defaultFormat($goods);
($this->format)($goods, $this);
}
return $goods;
}
/**
* 获取单个商品,找不到抛出异常
*
* @param bool $is_cache
* @return Goods
*/
public function findOrFail($is_cache = false)
{
$this->md5s[] = 'find';
$goods = $this->query->cache($this->getCacheKey($is_cache), (200 + mt_rand(0, 100)))->find();
if (!$goods) {
error_stop('商品不存在');
}
if ($this->format instanceof \Closure) {
// 格式化数据
$goods = $this->defaultFormat($goods);
($this->format)($goods, $this);
}
return $goods;
}
/**
* 把活动相关数据覆盖到商品
*
* @param array|object $goods
* @return array
*/
public function defaultFormat($goods)
{
$skuPrices = $goods->sku_prices;
$activity = $this->is_activity ? $goods->activity : null;
if ($activity) {
$skuPrices = ActivityFacade::recoverSkuPrices($goods, $activity);
// unset($goods['activity']['activity_sku_prices']); // db 获取活动这里删除报错
}
if ($this->show_score_shop) {
$scoreSkuPrices = $goods->all_score_sku_prices; // 包含下架的积分规格
// 积分商城,覆盖积分商城规格
foreach ($skuPrices as $key => &$skuPrice) {
$stock = $skuPrice->stock; // 下面要用
$skuPrice->stock = 0;
$skuPrice->sales = 0;
foreach ($scoreSkuPrices as $scoreSkuPrice) {
if ($skuPrice->id == $scoreSkuPrice->goods_sku_price_id) {
$skuPrice->stock = ($scoreSkuPrice->stock > $stock) ? $stock : $scoreSkuPrice->stock; // 积分商城库存不能超过商品库存
$skuPrice->sales = $scoreSkuPrice->sales;
$skuPrice->price = $scoreSkuPrice->price;
$skuPrice->score = $scoreSkuPrice->score;
$skuPrice->status = $scoreSkuPrice->status; // 采用积分的上下架
// $skuPrice->score_price = $scoreSkuPrice->score_price;
// 记录对应活动的规格的记录
$skuPrice->item_goods_sku_price = $scoreSkuPrice;
break;
}
}
}
}
// 移除下架的规格
foreach ($skuPrices as $key => $skuPrice) {
if ($skuPrice['status'] != 'up') {
unset($skuPrices[$key]);
}
}
$skuPrices = $skuPrices instanceof \think\Collection ? $skuPrices->values() : array_values($skuPrices);
if ($activity) {
// 处理活动相关的价格,销量等
// 这里由 getPriceAttr 计算,这里后续删除
// $prices = $skuPrices instanceof \think\Collection ? $skuPrices->column('price') : array_column($skuPrices, 'price');
// $goods['price'] = $prices ? min($prices) : $goods['price']; // min 里面不能是空数组
// if ($activity['type'] == 'groupon') {
// $grouponPrices = $skuPrices instanceof \think\Collection ? $skuPrices->column('groupon_price') : array_column($skuPrices, 'groupon_price');
// $goods['groupon_price'] = $grouponPrices ? min($grouponPrices) : $goods['price'];
// }
// if ($activity['type'] == 'groupon_ladder') {
// // @sn 阶梯拼团,商品详情如何显示拼团价格,阶梯拼团
// }
// if ($activity['rules'] && isset($activity['rules']['sales_show_type']) && $activity['rules']['sales_show_type'] == 'real') {
// // 活动设置显示真实销量
// $goods['sales'] = array_sum($skuPrices instanceof \think\Collection ? $skuPrices->column('sales') : array_column($skuPrices, 'sales'));
// } else {
// // 活动显示总销量
// $goods['sales'] += $goods['show_sales'];
// }
} elseif ($this->show_score_shop) {
// 积分商城这里显示的是真实销量, 目前都由 getSalesAttr 计算
// $goods['sales'] = array_sum($skuPrices instanceof \think\Collection ? $skuPrices->column('sales') : array_column($skuPrices, 'sales'));
} else {
// 没有活动,商品销量,加上虚增销量
// $goods['sales'] += $goods['show_sales'];
}
if ($this->show_score_shop) {
// 积分商城商品
$goods['show_score_shop'] = $this->show_score_shop;
}
// 不给 sku_prices 赋值,会触发 getSkuPricesAttr 计算属性,覆盖计算好的 sku_prices| 但是这样 sku_prices 会存在下标不连续的情况
$goods['new_sku_prices'] = $skuPrices; // 商品详情接口用, 过滤掉了 下架的规格
// $goods['sales'] = $goods->salesStockFormat($goods['sales'], $goods['sales_show_type']); // 格式化销量,前端格式化,这里可删除
$stocks = $skuPrices instanceof \think\Collection ? $skuPrices->column('stock') : array_column($skuPrices, 'stock'); // 获取规格中的库存
$stock = array_sum($stocks);
$goods['stock'] = $stock;
// $goods['stock'] = $goods->salesStockFormat($stock, $goods['stock_show_type']); // 格式化库存,前端格式化,这里可删除
$goods['activity_type'] = $activity['type'] ?? null;
return $goods;
}
/**
* 获取缓存 key
*
* @param bool $is_cache 是否缓存
* @return string|bool
*/
protected function getCacheKey($is_cache = false)
{
if ($is_cache) {
sort($this->md5s);
$key = 'goods-service-' . md5(json_encode($this->md5s));
}
return $key ?? false;
}
/**
* 默认调用 query 中的方法,比如 withTrashed()
*
* @param [type] $funcname
* @param [type] $arguments
* @return void
*/
public function __call($funcname, $arguments)
{
$this->query->{$funcname}(...$arguments);
return $this;
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,518 @@
<?php
namespace addons\shopro\service\order;
use app\admin\model\shopro\order\Order;
use app\admin\model\shopro\order\OrderItem;
use app\admin\model\shopro\order\Action as OrderAction;
use app\admin\model\shopro\order\Express as OrderExpress;
use app\admin\model\shopro\activity\Groupon as ActivityGroupon;
use app\admin\model\shopro\activity\GrouponLog as ActivityGrouponLog;
use app\admin\model\shopro\dispatch\Dispatch as DispatchModel;
use app\admin\model\shopro\dispatch\DispatchAutosend;
use addons\shopro\library\express\Express as ExpressLib;
class OrderDispatch
{
public $order = null;
public function __construct($params = [])
{
if (isset($params['order_id']) && !empty($params['order_id'])) {
$this->order = Order::find($params['order_id']);
if (!$this->order) {
error_stop('订单不存在');
}
}
}
/**
* 执行发货
*
* @return object
*/
public function confirm($params)
{
$admin = auth_admin();
$method = $params['method'] ?? '';
if (!in_array($method, ['input', 'api', 'upload'])) {
error_stop('请使用正确的发货方式');
}
if ($this->order->status !== Order::STATUS_PAID && !$this->order->isOffline($this->order)) {
error_stop("该订单{$this->order->status_text},不能发货");
}
if ($this->order->apply_refund_status === Order::APPLY_REFUND_STATUS_APPLY) {
error_stop("该订单已申请退款,暂不能发货");
}
switch ($method) {
case 'api':
list($orderItems, $express) = $this->doByApi($params);
break;
case 'input':
list($orderItems, $express) = $this->doByInput($params);
break;
case 'upload':
list($orderItems, $express) = $this->doByUpload($params);
break;
}
// 添加包裹信息
$orderExpress = OrderExpress::create([
'user_id' => $this->order->user_id,
'order_id' => $this->order->id,
'express_name' => $express['name'],
'express_code' => $express['code'],
'express_no' => $express['no'],
'method' => $method,
'driver' => $express['driver'] ?? null,
'ext' => $express['ext'] ?? null
]);
// 修改订单商品发货状态
foreach ($orderItems as $orderItem) {
$orderItem->order_express_id = $orderExpress->id;
$orderItem->dispatch_status = OrderItem::DISPATCH_STATUS_SENDED;
$orderItem->ext = array_merge($orderItem->ext, ['send_time' => time()]); // item 发货时间
$orderItem->save();
OrderAction::add($this->order, $orderItem, $admin, 'admin', "商品{$orderItem->goods_title}已发货");
}
$this->subscribeExpressInfo($orderExpress);
// 订单发货后
$data = [
'order' => $this->order,
'items' => $orderItems,
'express' => $orderExpress,
'dispatch_type' => 'express',
];
\think\Hook::listen('order_dispatch_after', $data);
return $express;
}
// 手动发货
private function doByInput($params)
{
$orderItems = $this->getDispatchOrderItems($params, 'express');
$express = $params['express'] ?? null;
if (empty($express['name']) || empty($express['code']) || empty($express['no']) || strpos($express['no'], '=') !== false) {
error_stop('请输入正确的快递信息');
}
return [$orderItems, $express];
}
// API发货
private function doByApi($params)
{
$orderItems = $this->getDispatchOrderItems($params, 'express');
$sender = $params['sender'] ?? null;
$expressLib = new ExpressLib();
$data = [
'order' => $this->order,
'sender' => $sender,
'consignee' => $this->order->address
];
$express = $expressLib->eOrder($data, $orderItems);
return [$orderItems, $express];
}
// 上传发货模板发货 TODO: 如果发货单比较多,循环更新可能会比较慢,考虑解析完模版信息以后,把数据返回前端,再次执行批量发货流程
private function doByUpload($params)
{
$orderItems = $this->getDispatchOrderItems($params, 'express');
$express = $params['express'] ?? null;
if (empty($express['name']) || empty($express['code']) || empty($express['no'])) {
error_stop('请输入正确的快递信息');
}
return [$orderItems, $express];
}
// 获取可发货的订单商品
public function getDispatchOrderItems($params = null, $dispatch_type = 'express')
{
$orderItemIds = $params['order_item_ids'] ?? [];
$whereCanDispatch['order_id'] = $this->order->id;
$whereCanDispatch['dispatch_status'] = OrderItem::DISPATCH_STATUS_NOSEND;
$whereCanDispatch['aftersale_status'] = ['<>', OrderItem::AFTERSALE_STATUS_ING];
$whereCanDispatch['refund_status'] = OrderItem::REFUND_STATUS_NOREFUND;
$whereCanDispatch['dispatch_type'] = $dispatch_type;
if (empty($orderItemIds)) {
$orderItems = OrderItem::where($whereCanDispatch)->select();
} else {
$orderItems = OrderItem::where('id', 'in', $orderItemIds)->where($whereCanDispatch)->select();
if (count($orderItems) !== count($orderItemIds)) {
error_stop('选中商品暂不能发货');
}
}
if (!$orderItems) {
error_stop('该订单无可发货商品');
}
return $orderItems;
}
/**
* 取消发货
*
*/
public function cancel($params)
{
$admin = auth_user();
$order_express_id = $params['order_express_id'] ?? 0;
$orderExpress = OrderExpress::where('id', $order_express_id)->find();
if (!$orderExpress) {
error_stop('未找到发货单');
}
// 1.检测是不是用api发的 有些快递不支持取消接口 所以不判断了,统一手动取消
// if ($orderExpress->method === 'api') {
// // TODO: 走取消运单接口
// $expressLib = new ExpressLib();
// $data = [
// 'express_no' => $orderExpress['express_no'],
// 'express_code' => $orderExpress['express_code'],
// 'order_code' => $orderExpress['ext']['Order']['OrderCode']
// ];
// $express = $expressLib->cancel($data);
// }
// 2. 变更发货状态
$orderItems = OrderItem::where([
'order_id' => $this->order->id,
'order_express_id' => $orderExpress->id
])->where('dispatch_type', 'express')->select();
foreach ($orderItems as $orderItem) {
$orderItem->order_express_id = 0;
$orderItem->dispatch_status = OrderItem::DISPATCH_STATUS_NOSEND;
$orderItem->save();
OrderAction::add($this->order, null, $admin, 'admin', "已取消发货");
}
// 删除发货单
$orderExpress->delete();
return true;
}
/**
* 修改发货信息
*
*/
public function change($params)
{
$admin = auth_user();
$order_express_id = $params['order_express_id'] ?? 0;
$orderExpress = OrderExpress::where('id', $order_express_id)->find();
if (!$orderExpress) {
error_stop('未找到发货单');
}
// 1.1 检测是不是用api发的 如果是则提醒取消运单再重新走发货流程 此时什么都不用做
if ($orderExpress->method === 'api') {
error_stop('该发货单已被推送第三方平台,请取消后重新发货');
}
// 1.2 如果不是则手动变更运单信息(快递公司、运单号)
$express = $params['express'] ?? null;
if (empty($express['name']) || empty($express['code']) || empty($express['no']) || strpos($express['no'], '=') !== false) {
error_stop('请输入正确的快递信息');
}
$orderExpress->save([
'express_name' => $express['name'],
'express_code' => $express['code'],
'express_no' => $express['no'],
'method' => 'input'
]);
OrderAction::add($this->order, null, $admin, 'admin', "变更发货信息");
$this->subscribeExpressInfo($orderExpress);
// 修改发货信息
$data = [
'order' => $this->order,
'express' => $orderExpress,
'dispatch_type' => 'express',
];
\think\Hook::listen('order_dispatch_change', $data);
return $express;
}
// 解析批量发货信息,筛选出能发货的订单
public function multiple($params)
{
// 上传发货模板
if (!empty($params['file'])) {
$express = $params['express'];
$file = $params['file']->getPathname();
$reader = new \PhpOffice\PhpSpreadsheet\Reader\Xlsx();
$PHPExcel = $reader->load($file);
$currentSheet = $PHPExcel->getSheet(0); //读取文件中的第一个工作表
$allRow = $currentSheet->getHighestRow(); //取得一共有多少行
if ($allRow <= 2) {
error_stop('您的发货列表为空');
}
$orderExpressMap = [];
$orderId = 0;
$orderSn = "";
for ($currentRow = 2; $currentRow <= $allRow - 1; $currentRow++) {
$orderId = $currentSheet->getCellByColumnAndRow(1, $currentRow)->getValue() ?? $orderId;
$orderSn = $currentSheet->getCellByColumnAndRow(2, $currentRow)->getValue() ?? $orderSn;
$orderItemId = $currentSheet->getCellByColumnAndRow(8, $currentRow)->getValue();
if (empty($orderItemId)) {
error_stop("发货单格式不正确");
}
$orderExpressNo = $currentSheet->getCellByColumnAndRow(15, $currentRow)->getValue();
if (empty($orderExpressNo)) {
error_stop("请填写 订单ID-{$orderId} 的运单号");
}
$orderExpressMap["$orderId.$orderSn"][$orderExpressNo][] = $orderItemId;
}
$list = [];
foreach ($orderExpressMap as $orderFlag => $orderExpress) {
foreach ($orderExpress as $expressNo => $orderItemIds) {
$order = explode('.', $orderFlag);
$list[] = [
'order_id' => $order[0],
'order_sn' => $order[1],
'order_item_ids' => $orderItemIds,
'express' => [
'name' => $express['name'],
'code' => $express['code'],
'no' => $expressNo
]
];
}
}
return $list;
} else {
$list = [];
$orderIds = $params['order_ids'] ?? [];
if (empty($orderIds)) {
error_stop('请选择发货订单');
}
foreach ($orderIds as $orderId) {
$list[] = [
'order_id' => $orderId,
'order_sn' => Order::where('id', $orderId)->value('order_sn'),
'order_item_ids' => $this->getDispatchOrderItemIds($orderId)
];
}
return $list;
}
}
// 获取可发货的订单商品
private function getDispatchOrderItemIds($orderId)
{
$whereCanDispatch = [
'order_id' => $orderId,
'dispatch_status' => OrderItem::DISPATCH_STATUS_NOSEND,
'aftersale_status' => ['neq', OrderItem::AFTERSALE_STATUS_ING],
'refund_status' => OrderItem::REFUND_STATUS_NOREFUND,
'dispatch_type' => 'express'
];
$orderItems = OrderItem::where($whereCanDispatch)->column('id');
return $orderItems;
}
// 订阅物流追踪
private function subscribeExpressInfo($orderExpress)
{
try {
$expressLib = new ExpressLib();
$a = $expressLib->subscribe([
'express_code' => $orderExpress['express_code'],
'express_no' => $orderExpress['express_no']
]);
} catch (\Exception $e) {
// Nothing TODO
return;
}
}
/**
* 手动发货
*
* @param array $params
* @return void
*/
public function customDispatch($params)
{
$admin = auth_admin();
$custom_type = $params['custom_type'] ?? 'text';
$custom_content = $params['custom_content'] ?? ($custom_type == 'text' ? '' : []);
if ($this->order->status !== Order::STATUS_PAID && !$this->order->isOffline($this->order)) {
error_stop("该订单{$this->order->status_text},不能发货");
}
if ($this->order->apply_refund_status === Order::APPLY_REFUND_STATUS_APPLY) {
error_stop("该订单已申请退款,暂不能发货");
}
// 获取手动发货的 items
$orderItems = $this->getDispatchOrderItems($params, 'custom');
$customExt = [ // 手动发货信息
'dispatch_content_type' => $custom_type,
'dispatch_content' => $custom_content
];
// 修改订单商品发货状态
foreach ($orderItems as $orderItem) {
$orderItem->dispatch_status = OrderItem::DISPATCH_STATUS_SENDED;
$orderItem->ext = array_merge($orderItem->ext, $customExt, ['send_time' => time()]); // item 发货时间
$orderItem->save();
OrderAction::add($this->order, $orderItem, $admin, 'admin', "商品{$orderItem->goods_title}已发货");
}
// 订单发货后
$data = [
'order' => $this->order,
'items' => $orderItems,
'express' => null,
'dispatch_type' => 'custom',
];
\think\Hook::listen('order_dispatch_after', $data);
}
/**
* 拼团完成时触发检测自动发货
*
* @return bool
*/
public function grouponCheckDispatchAndSend()
{
$this->systemCheckAutoSend();
return true;
}
/**
* 普通商品自动发货
*
* @return bool
*/
public function checkDispatchAndSend()
{
// 拼团不自动发货,等成团完成才发货
$orderExt = $this->order['ext'];
$buy_type = ($orderExt && isset($orderExt['buy_type'])) ? $orderExt['buy_type'] : '';
if ($this->order['activity_type'] && strpos($this->order['activity_type'], 'groupon') !== false && $buy_type == 'groupon') {
return true; // 这里不对拼团的订单进行自动发货,等拼团成功在检测
}
// 检测需要自动发货的 item
$this->systemCheckAutoSend();
return true;
}
/**
* 系统检测自动发货
*/
private function systemCheckAutoSend()
{
$autosendItems = [];
// 判断订单是否有需要发货的商品并进行自动发货autosend
foreach ($this->order->items as $key => $item) {
// 判断不是未发货状态或者退款完成continue
if (
$item['dispatch_status'] == OrderItem::DISPATCH_STATUS_NOSEND
&& $item['aftersale_status'] != OrderItem::AFTERSALE_STATUS_ING
&& $item['refund_status'] == OrderItem::REFUND_STATUS_NOREFUND
) {
// 订单可以发货
switch ($item['dispatch_type']) {
case 'autosend':
// 自动发货
$autosendItems[] = $item;
}
}
}
if ($autosendItems) {
$this->autoSendItems($autosendItems, ['oper_type' => 'system']);
}
}
/**
* 当前订单需要自动发货的所有商品
*
* @param object|array $items
* @return void
*/
private function autoSendItems($items)
{
foreach ($items as $item) {
$autosendExt = $this->getAutosendContent($item);
$item->dispatch_status = OrderItem::DISPATCH_STATUS_SENDED;
$item->ext = array_merge($item->ext, $autosendExt, ['send_time' => time()]); // item 发货时间
$item->save();
OrderAction::add($this->order, $item, null, 'system', "商品{$item->goods_title}已发货");
}
$data = [
'order' => $this->order,
'items' => $items,
'express' => null,
'dispatch_type' => 'custom',
];
// 发货后事件,消息通知
\think\Hook::listen('order_dispatch_after', $data);
}
/**
* 获取商品的自动发货模板数据
*
* @param object|array $item
* @return array
*/
private function getAutosendContent($item)
{
// 获取配送模板
$result = [];
$dispatch = DispatchModel::with([$item['dispatch_type']])->show()->where('type', $item['dispatch_type'])->where('id', $item['dispatch_id'])->find();
if ($dispatch && $dispatch->autosend) {
$autosend = $dispatch->autosend;
if (in_array($autosend['type'], ['text', 'params'])) {
$result['dispatch_content_type'] = $autosend['type'];
$result['dispatch_content'] = $autosend['content'];
}
}
return $result;
}
}

View File

@@ -0,0 +1,442 @@
<?php
namespace addons\shopro\service\order;
use app\admin\model\shopro\user\User;
use app\admin\model\shopro\order\Order;
use app\admin\model\shopro\order\OrderItem;
use app\admin\model\shopro\order\Action;
use app\admin\model\shopro\goods\Comment;
use app\admin\model\shopro\activity\GiftLog;
use app\admin\model\shopro\activity\Order as ActivityOrderModel;
use app\admin\model\shopro\order\Invoice as OrderInvoice;
class OrderOper
{
public function __construct()
{
}
/**
* 订单自动关闭`
*
* @param \think\Model $order
* @param \think\Model|null $user
* @param string $type
* @return void
*/
public function close($order, $user = null, $type = 'user', $msg = '', $ext = [])
{
$order->status = Order::STATUS_CLOSED;
$order->ext = array_merge($order->ext, $ext, ['closed_time' => time()]); // 关闭时间
$order->allowField(true)->save();
Action::add($order, null, $user, $type, ($msg ? : $this->getOperText($type) . '关闭订单'));
// 订单自动关闭之后 行为 返还用户优惠券,积分
$data = ['order' => $order];
\think\Hook::listen('order_close_after', $data);
return $order;
}
/**
* 订单取消
*
* @param object $order
* @param object $user
* @param string $type
* @return object
*/
public function cancel($order, $user = null, $type = 'user', $msg = '')
{
$order->status = Order::STATUS_CANCEL; // 取消订单
$order->ext = array_merge($order->ext, ['cancel_time' => time()]); // 取消时间
$order->allowField(true)->save();
Action::add($order, null, $user, $type, ($msg ?: $this->getOperText($type) . '取消订单'));
// 订单取消,退回库存,退回优惠券积分,等
$data = ['order' => $order];
\think\Hook::listen('order_cancel_after', $data);
return $order;
}
/**
* 申请全额退款
*
* @param object $order
* @param object $user
* @param string $type
* @return object
*/
public function applyRefund($order, $user, $type = 'user')
{
$items = OrderItem::where('order_id', $order->id)->lock(true)->select();
foreach ($items as $key => $item) {
if (in_array($item['refund_status'], [
OrderItem::REFUND_STATUS_AGREE,
OrderItem::REFUND_STATUS_COMPLETED,
])) {
error_stop('订单有退款,不可申请');
}
if ($item['dispatch_status'] != OrderItem::DISPATCH_STATUS_NOSEND) {
error_stop('订单已发货,不可申请');
}
}
$order->apply_refund_status = Order::APPLY_REFUND_STATUS_APPLY; // 申请退款
$order->ext = array_merge($order->ext, ['apply_refund_time' => time()]); // 申请时间
$order->save();
Action::add($order, null, $user, $type, $this->getOperText($type) . '申请全额退款');
// 订单申请全额退款
$data = ['order' => $order, 'user' => $user];
\think\Hook::listen('order_apply_refund_after', $data);
return $order;
}
/**
* 确认收货
*
* @param object $order
* @param array $itemIds
* @param object|null $user
* @param string $type
* @return object
*/
public function confirm($order, $itemIds = [], $user = null, $type = 'user')
{
$items = OrderItem::canConfirm()->where('order_id', $order->id)->where(function ($query) use ($itemIds) {
if ($itemIds) {
// 只确认收货传入的 ids
$query->whereIn('id', $itemIds);
}
})->lock(true)->select();
if (!$items) {
error_stop('订单已确认收货,请不要重复确认收货');
}
foreach ($items as $item) {
$item->ext = array_merge($item->ext, ['confirm_time' => time()]);
$item->dispatch_status = OrderItem::DISPATCH_STATUS_GETED; // 确认收货
$item->save();
Action::add($order, $item, $user, $type, $this->getOperText($type) . '确认收货');
// 订单确认收货后
$data = ['order' => $order, 'item' => $item];
\think\Hook::listen('order_confirm_after', $data);
}
return $order;
}
/**
* 拒收收货
*
* @param object $order
* @param array $itemIds
* @param object|null $user
* @param string $type
* @return object
*/
public function refuse($order, $user = null, $type = 'user')
{
$items = OrderItem::canConfirm()->where('order_id', $order->id)->lock(true)->select();
if (!$items) {
error_stop('没有可拒收的商品');
}
foreach ($items as $item) {
$item->ext = array_merge($item->ext, ['refuse_time' => time()]);
$item->dispatch_status = OrderItem::DISPATCH_STATUS_REFUSE; // 拒收
$item->save();
Action::add($order, $item, $user, $type, $this->getOperText($type) . '操作,用户拒绝收货');
}
// 订单拒收后事件
$data = ['order' => $order];
\think\Hook::listen('order_refuse_after', $data);
return $order;
}
/**
* 评价 (根据 comments 中的数据进行评价, 可以之评价一个(系统自动评价时候))
*
* @param object $order
* @param object|null $user
* @param string $type
* @return object
*/
public function comment($order, $comments, $user = null, $type = 'user')
{
// 评价的orderItem id
$comments = array_column($comments, null, 'item_id');
$itemIds = array_keys($comments);
$items = OrderItem::canComment()->where('order_id', $order->id)->lock(true)->select();
if (!$items) {
if ($type == 'system') {
return $order; // 系统自动评价时检测到用户已经自己评价,这里直接返回,不抛出异常
}
error_stop('订单已评价,请不要重复评价');
}
$orderConfig = sheep_config('shop.order');
$orderUser = User::get($order->user_id);
foreach ($items as $item) {
if (!in_array($item['id'], $itemIds)) {
// 不在本次评价列表
continue;
}
$comment = $comments[$item['id']] ?? [];
$status = 'normal';
if (isset($orderConfig['comment_check']) && $orderConfig['comment_check'] && $type != 'system') {
// 需要检查,并且不是系统自动评价
$status = 'hidden';
}
Comment::create([
'goods_id' => $item->goods_id,
'order_id' => $order->id,
'order_item_id' => $item->id,
'user_id' => $order->user_id,
'user_nickname' => $orderUser ? $orderUser->nickname : null,
'user_avatar' => $orderUser ? $orderUser->avatar : null,
'level' => $comment['level'] ?? 5,
'content' => $comment['content'] ?? ($orderConfig['auto_comment_content'] ?? '用户默认好评'),
'images' => $comment['images'] ?? [],
'status' => $status
]);
$item->ext = array_merge($item->ext, ['comment_time' => time()]);
$item->comment_status = OrderItem::COMMENT_STATUS_OK; // 评价
$item->save();
Action::add($order, $item, $user, $type, $this->getOperText($type) . '评价完成');
// 订单评价后
$data = ['order' => $order, 'item' => $item, 'user' => $user];
\think\Hook::listen('order_comment_after', $data);
}
return $order;
}
/**
* 订单申请发票
*
* @param \think\Model $order
* @param \think\Model $userInvoice
* @param mixed $user
* @param string $type
* @return \think\Model
*/
public function applyInvoice($order, $userInvoice, $user = null, $type = 'user')
{
$ext = $order->ext;
if ($order->invoice_status !== 0 || !in_array($order->status_code, ['commented', 'nocomment', 'noget', 'nosend', 'completed'])) {
// 可开票但是未开票,并且订单状态已支付,未退款
error_stop('当前订单不可开票');
}
// 保存收货地址
$orderInvoice = new OrderInvoice();
$orderInvoice->type = $userInvoice->type;
$orderInvoice->order_id = $order->id;
$orderInvoice->user_id = $userInvoice->user_id;
$orderInvoice->name = $userInvoice->name;
$orderInvoice->tax_no = $userInvoice->tax_no;
$orderInvoice->address = $userInvoice->address;
$orderInvoice->mobile = $userInvoice->mobile;
$orderInvoice->bank_name = $userInvoice->bank_name;
$orderInvoice->bank_no = $userInvoice->bank_no;
$orderInvoice->amount = $ext['invoice_amount'];
$orderInvoice->status = 'waiting';
$orderInvoice->save();
$order->invoice_status = 1;
$order->save();
Action::add($order, null, $user, $type, $this->getOperText($type) . '申请开票');
return $order;
}
/**
* 删除订单
*
* @param object $order
* @param object|null $user
* @param string $type
* @return void
*/
public function delete($order, $user = null, $type = 'user')
{
$order->delete(); // 删除订单
Action::add($order, null, $user, 'user', '用户删除订单');
}
/**
* 存储活动信息
*
* @param \think\Model $order
* @param array $result
* @return void
*/
public function addActivityOrder($order)
{
$ext = $order->ext;
$items = $order->items;
$goodsIds = array_column($items, 'goods_id');
$model = new ActivityOrderModel();
$model->order_id = $order->id;
$model->user_id = $order->user_id;
$model->activity_id = $order->activity_id;
$model->activity_title = $ext['activity_title'] ?? null;
$model->activity_type = $ext['activity_type'] ?? null;
$model->pay_fee = $order->pay_fee;
$model->goods_amount = $order->goods_amount;
$model->discount_fee = $ext['activity_discount_amount'] ?? 0; // 普通商品总额和活动时商品总额的差
$model->goods_ids = join(',', $goodsIds);
$model->save();
}
/**
* 存储促销信息
*
* @param \think\Model $order
* @param array $result
* @return void
*/
public function addPromosOrder($order)
{
$ext = $order->ext;
$promoInfos = $ext['promo_infos'] ?? [];
foreach ($promoInfos as $key => $info) {
$model = new ActivityOrderModel();
$model->order_id = $order->id;
$model->user_id = $order->user_id;
$model->activity_id = $info['activity_id'];
$model->activity_title = $info['activity_title'];
$model->activity_type = $info['activity_type'];
$model->pay_fee = $order->pay_fee;
$model->goods_amount = $info['promo_goods_amount'];
$model->discount_fee = 0;
if (in_array($info['activity_type'], ['full_reduce', 'full_discount', 'free_shipping'])) {
$model->discount_fee = $info['promo_discount_money'];
} else if ($info['activity_type'] == 'full_gift') {
// 这里设置为 0等支付成功之后补充
$model->discount_fee = 0;
}
$model->goods_ids = join(',', $info['goods_ids']);
$rules = [
'rule_type' => $info['rule_type'],
'discount_rule' => $info['discount_rule'],
];
if ($info['activity_type'] == 'full_gift') {
$rules['limit_num'] = $info['limit_num'] ?? 0;
$rules['event'] = $info['event'] ?? 0;
}
$currentExt['rules'] = $rules;
$model->ext = $currentExt;
$model->save();
}
}
/**
* 将活动订单标记为已支付
*
* @param [type] $order
* @return void
*/
public function activityOrderPaid($order)
{
$activityOrders = ActivityOrderModel::where('order_id', $order->id)->select();
foreach ($activityOrders as $activityOrder) {
if ($activityOrder->activity_type == 'full_gift') {
$value_money = GiftLog::where('activity_id', $activityOrder->activity_id)
->where('order_id', $activityOrder->order_id)
->where('user_id', $activityOrder->user_id)
->whereIn('type', ['money', 'coupon']) // 这里只算 赠送的余额,和优惠券(不算积分,和赠送商品的价值)
->sum('value');
$activityOrder->discount_fee = $value_money; // 补充赠送的价值
} else if (in_array($activityOrder->activity_type, ['groupon', 'groupon_ladder'])) {
$ext = $order->ext;
$currentExt['buy_type'] = $ext['buy_type'] ?? '';
$currentExt['groupon_id'] = $ext['groupon_id'] ?? 0; // 开团时候,支付之后才会有 groupon_id
$activityOrder->ext = $currentExt;
}
$activityOrder->status = ActivityOrderModel::STATUS_PAID;
$activityOrder->save();
}
}
/**
* 根据 oper_type 获取对应的用户
*/
private function getOperText($oper_type)
{
switch($oper_type) {
case 'user':
$oper_text = '用户';
break;
case 'admin':
$oper_text = '管理员';
break;
case 'system':
$oper_text = '系统自动';
break;
default :
$oper_text = '系统自动';
break;
}
return $oper_text;
}
}

View File

@@ -0,0 +1,247 @@
<?php
namespace addons\shopro\service\order;
use app\admin\model\shopro\order\Order;
use app\admin\model\shopro\order\OrderItem;
use app\admin\model\shopro\order\Action;
use app\admin\model\shopro\Pay as PayModel;
use app\admin\model\shopro\user\User;
use app\common\model\User as CommonUser;
use addons\shopro\service\pay\PayRefund;
use addons\shopro\service\pay\PayOper;
use addons\shopro\traits\CouponSend;
use addons\shopro\service\StockSale;
use addons\shopro\facade\Activity as ActivityFacade;
class OrderRefund
{
use CouponSend;
protected $order = null;
protected $default_refund_type = 'back';
public function __construct($order)
{
$this->order = $order;
$this->default_refund_type = $order->ext['refund_type'] ?? 'back';
}
/**
* 全额退款(无条件退款, 优惠券积分全退)
*
* @param \think\Model $user
* @param string $remark
* @return void
*/
public function fullRefund($user = null, $data = [])
{
$items = OrderItem::where('order_id', $this->order->id)->lock(true)->select();
foreach ($items as $key => $item) {
if (in_array($item['refund_status'], [
OrderItem::REFUND_STATUS_AGREE,
OrderItem::REFUND_STATUS_COMPLETED,
])) {
error_stop('订单有退款,不能全额退款');
}
}
// 返还库存,减少销量
$stockSale = new StockSale();
$stockSale->backStockSale($this->order, $items);
if ($this->order->apply_refund_status == Order::APPLY_REFUND_STATUS_APPLY) {
// 如果订单申请了全额退款,这里将全额退款状态改为 已完成
$this->order->apply_refund_status = Order::APPLY_REFUND_STATUS_FINISH;
$this->order->save();
}
if ($this->order->coupon_id) {
// 订单使用了优惠券,退回用户优惠券
$this->backUserCoupon($this->order->coupon_id);
}
if ($this->order->activity_id || $this->order->promo_types) {
// 有活动,执行活动失败
ActivityFacade::buyFail($this->order, 'refund');
}
$total_refund_fee = '0'; // 已退金额
foreach ($items as $key => $item) {
$is_last = (count($items) - 1) == $key ? true : false; // 是否最后一个商品
// 计算 refund_fee
$refund_fee = $this->calcRefundFee($item, $total_refund_fee, $is_last);
$item->refund_status = OrderItem::REFUND_STATUS_AGREE; // 同意退款
$item->refund_fee = $refund_fee; // 实时计算,总 退款金额 等于 商品实际支付金额
$item->ext = array_merge($item->ext, ['refund_time' => time()]); // 退款时间
$item->save();
// 累加已退金额
if ($refund_fee > 0) {
$total_refund_fee = bcadd($total_refund_fee, $refund_fee, 2);
}
Action::add($this->order, $item, $user, ($user ? (($user instanceof User || $user instanceof CommonUser) ? 'user' : 'admin') : 'system'), (isset($data['remark']) && $data['remark'] ? $data['remark'] . ',' : '') . '退款金额:¥' . $item->refund_fee);
// 订单商品退款后
$eventData = ['order' => $this->order, 'item' => $item];
\think\Hook::listen('order_item_refund_after', $eventData);
}
// 退回已支付的所有金额(积分,余额等)
$this->refundAllPays($data);
// 订单商品退款后
$eventData = [
'order' => $this->order,
'items' => $items,
'refund_type' => $data['refund_type'] ?? $this->default_refund_type,
'refund_method' => 'full_refund'
];
\think\Hook::listen('order_refund_after', $eventData);
}
/**
* 部分退款 (通过订单商品退款)
*
* @param \think\Model $item
* @param string $refund_money
* @param \think\Model $user
* @param string $remark
* @return void
*/
public function refund($item, $refund_money, $user = null, $data = [])
{
$item->refund_status = OrderItem::REFUND_STATUS_AGREE; // 同意退款
$item->refund_fee = $refund_money;
$item->ext = array_merge($item->ext, ['refund_time' => time()]); // 退款时间
$item->save();
Action::add($this->order, $item, $user, ($user ? (($user instanceof User || $user instanceof CommonUser) ? 'user' : 'admin') : 'system'), (isset($data['remark']) && $data['remark'] ? $data['remark'] . ',' : '') . '退款金额:¥' . $refund_money);
// 订单商品退款后
$eventData = ['order' => $this->order, 'item' => $item];
\think\Hook::listen('order_item_refund_after', $eventData);
// 查找符合条件的 pays 并从中退指定金额
$this->refundPaysByMoney((string)$refund_money, $data);
// 订单商品退款后
$eventData = [
'order' => $this->order,
'items' => [$item],
'refund_type' => $data['refund_type'] ?? $this->default_refund_type,
'refund_method' => 'item_refund'
];
\think\Hook::listen('order_refund_after', $eventData);
}
/**
* 退回已支付的所有 pays 记录
*
* @param string $remark
* @return void
*/
public function refundAllPays($data = [])
{
// 商城订单,已支付的 pay 记录
$pays = PayModel::typeOrder()->paid()->where('order_id', $this->order->id)->lock(true)->select();
$refund = new PayRefund($this->order->user_id);
foreach ($pays as $key => $pay) {
$refund->fullRefund($pay, [
'refund_type' => $data['refund_type'] ?? $this->default_refund_type,
'platform' => $this->order->platform,
'remark' => $data['remark'] ?? ''
]);
}
}
/**
* 查找符合条件的 pays 并从中退指定金额 (不退积分,包括积分抵扣的积分)
*
* @param string $refund_money
* @param string $remark
* @return void
*/
protected function refundPaysByMoney(string $refund_money, $data = [])
{
$payOper = new PayOper($this->order->user_id);
$pays = $payOper->getCanRefundPays($this->order->id);
$remain_max_refund_money = $payOper->getRemainRefundMoney($pays);
if (bccomp($refund_money, $remain_max_refund_money, 2) === 1) {
// 退款金额超出最大支付金额
error_stop('退款金额超出最大可退款金额');
}
$current_refunded_money = '0'; // 本次退款,已退金额累计
$refund = new PayRefund($this->order->user_id);
foreach ($pays as $key => $pay) {
$current_remain_money = bcsub($refund_money, $current_refunded_money, 2); // 剩余应退款金额
if ($current_remain_money <= 0) {
// 退款完成
break;
}
$current_pay_remain_money = bcsub($pay->pay_fee, $pay->refund_fee, 2); // 当前 pay 记录剩余可退金额
if ($current_pay_remain_money <= 0) {
// 当前 pay 支付的金额已经退完了,循环下一个
continue;
}
$current_refund_money = min($current_remain_money, $current_pay_remain_money); // 取最小值
$refund->refund($pay, $current_refund_money, [
'refund_type' => $data['refund_type'] ?? $this->default_refund_type,
'platform' => $this->order->platform,
'remark' => $data['remark'] ?? ''
]);
$current_refunded_money = bcadd($current_refunded_money, $current_refund_money, 2);
}
if ($refund_money > $current_refunded_money) {
// 退款金额超出最大支付金额
error_stop('退款金额超出最大可退款金额');
}
}
/**
* 计算 item 应退金额
*
* @param \think\Model $item
* @param string $total_refund_fee
* @param boolean $is_last
* @return string
*/
private function calcRefundFee($item, $total_refund_fee, $is_last = false)
{
$pay_fee = $this->order->pay_fee; // 支付总金额
$current_goods_amount = bcmul($item->goods_price, (string)$item->goods_num, 2);
$total_amount = bcadd($current_goods_amount, $item->dispatch_fee, 2);
$refund_fee = bcsub($total_amount, $item->discount_fee, 2); // (商品金额 + 运费金额) - 总优惠(活动,优惠券,包邮优惠)
if ($total_refund_fee >= $pay_fee) {
$refund_fee = 0;
} else {
$remain_fee = bcsub($pay_fee, $total_refund_fee, 2);
$refund_fee = $remain_fee > $refund_fee ? ($is_last ? $remain_fee : $refund_fee) : $remain_fee;
}
return $refund_fee;
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace addons\shopro\service\order;
use app\admin\model\shopro\order\OrderItem;
use app\admin\model\shopro\order\Order;
use addons\shopro\service\StockSale;
use addons\shopro\facade\Activity as ActivityFacade;
class OrderThrough
{
/**
* 商品限购
*
* @param [type] $buyInfo
* @param \Closure $next
* @return void
*/
public function limitBuy ($buyInfo, \Closure $next)
{
$user = auth_user();
$goods = $buyInfo['goods'];
$goods_num = $buyInfo['goods_num'] ?? 1;
$activity = $goods['activity'];
if ($activity) {
// 活动限购
$rules = $activity['rules'] ?? [];
$limit_type = 'activity';
$limit_num = (isset($rules['limit_num']) && $rules['limit_num'] > 0) ? $rules['limit_num'] : 0;
} else {
// 普通商品限购
$limit_type = $goods->limit_type;
$limit_num = ($limit_type != 'none' && $goods->limit_num > 0) ? $goods->limit_num : 0;
}
if ($limit_num) { // limit_num = 0; 不限购
// 查询用户老订单,判断本次下单数量,判断是否超过购买限制, 未支付的或者已完成的都算
$buy_num = OrderItem::where('user_id', $user['id'])->where('goods_id', $goods->id)
->where(function ($query) use ($limit_type, $goods, $activity) {
if ($limit_type == 'daily') {
// 按天限购
$daily_start = strtotime(date('Y-m-d'));
$daily_end = strtotime(date('Y-m-d', (time() + 86400))) - 1;
$query->where('createtime', 'between', [$daily_start, $daily_end]);
} else if ($limit_type == 'activity') {
$query->where('activity_id', $activity['id']); // 这个活动下所有的购买记录
} else {
// all不加任何条件
}
return $query;
})
->whereExists(function ($query) use ($goods) {
$order_table_name = (new Order())->getQuery()->getTable();
$query->table($order_table_name)->where('order_id=' . $order_table_name . '.id')
->whereNotIn('status', [Order::STATUS_CLOSED, Order::STATUS_CANCEL]); // 除了交易关闭,和 取消的订单
})->sum('goods_num');
if (($buy_num + $goods_num) > $limit_num) {
$msg = '该商品' . ($limit_type == 'daily' ? '每日' : ($limit_type == 'activity' ? '活动期间' : '')) . '限购 ' . $limit_num . ' 件';
if ($buy_num < $limit_num) {
$msg .= ',当前还可购买 ' . ($limit_num - $buy_num) . ' 件';
}
error_stop($msg);
}
}
return $next($buyInfo);
}
public function checkStock($buyInfo, \Closure $next)
{
$goods = $buyInfo['goods'];
$activity = $goods['activity'];
if (!$activity) {
$stockSale = new StockSale();
$stockSale->stockLock($buyInfo);
}
return $next($buyInfo);
}
public function activity($buyInfo, \Closure $next)
{
$goods = $buyInfo['goods'];
$activity = $goods['activity'];
if ($activity) {
$buyInfo = ActivityFacade::buy($buyInfo, $activity);
}
return $next($buyInfo);
}
public function through($throughs = [])
{
$throughs = is_array($throughs) ? $throughs : [$throughs];
$pipes = [];
foreach ($throughs as $through) {
if (method_exists($this, $through)) {
$pipes[] = function ($params, \Closure $next) use ($through) {
return $this->{$through}($params, $next);
};
}
}
return $pipes;
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace addons\shopro\service\order\shippingInfo;
use addons\shopro\exception\ShoproException;
use app\admin\model\shopro\Pay as PayModel;
use think\helper\Str;
class Base
{
protected $order = null;
public function __construct($order)
{
$this->order = $order;
}
/**
* 设置微信支付相关的参数
*
* @param array $uploadParams
* @param \think\Model $wechatPay
* @return array
*/
protected function setWechatParams($uploadParams, $wechatPay)
{
$order_key = [
'order_number_type' => 2,
'transaction_id' => $wechatPay->transaction_id,
'out_trade_no' => $wechatPay->pay_sn,
];
$payer = [
'openid' => $wechatPay['buyer_info']
];
foreach ($uploadParams as &$params) {
$params['order_key'] = $order_key;
$params['payer'] = $payer;
}
return $uploadParams;
}
/**
* 获取订单中的微信支付 pay 记录
*
* @return think\Model
*/
protected function getWechatPay($type = 'order')
{
$wechatPay = PayModel::{'type' . Str::studly($type)}()->where('order_id', $this->order['id'])
->where('status', '<>', PayModel::PAY_STATUS_UNPAID)
->where('pay_type', 'wechat')->order('id', 'desc')->find();
if (!$wechatPay) {
throw new ShoproException('未找到订单微信支付记录');
}
return $wechatPay;
}
/**
* 配送方式转换
*
* @param string $dispatch_type
* @return integer
*/
protected function getLogisticsType($dispatch_type)
{
switch ($dispatch_type) {
case 'express':
$logistics_type = 1;
break;
case 'store_delivery':
$logistics_type = 2;
break;
case 'autosend':
$logistics_type = 3;
break;
case 'custom':
$logistics_type = 3;
break;
case 'selfetch':
$logistics_type = 4;
break;
default:
$logistics_type = 1;
break;
}
return $logistics_type;
}
}

View File

@@ -0,0 +1,300 @@
<?php
namespace addons\shopro\service\order\shippingInfo;
use app\admin\model\shopro\order\OrderItem;
use app\admin\model\shopro\order\Express as OrderExpress;
use app\admin\model\shopro\order\Address as OrderAddress;
class OrderShippingInfo extends Base
{
protected $orderItems = null;
protected $dispatchTypes = [];
/**
* 获取整个订单的 shippingParams 参数
*
* @return array
*/
public function getShippingParams()
{
$wechatPay = $this->getWechatPay();
$this->setSendOrderItems();
$uploadParams = [];
if (in_array('express', $this->dispatchTypes)) {
// 有 快递物流 商品
$expressUploadParams = $this->getExpressShippingParams();
$uploadParams = array_merge($uploadParams, $expressUploadParams);
}
if (!$uploadParams && array_intersect(['autosend', 'custom'], $this->dispatchTypes)) {
// 有 自动发货,或者手动发货 商品
$virtualParams = $this->getVirtualShippingParams();
$uploadParams[] = $virtualParams;
}
if (!$uploadParams && in_array('selfetch', $this->dispatchTypes)) {
// 有 到店自提 商品
$selfParams = $this->getSelfetchShippingParams();
$uploadParams[] = $selfParams;
}
if (!$uploadParams && in_array('store_delivery', $this->dispatchTypes)) {
// 有 店铺配送 商品
$storeDeliveryParams = $this->getStoreDeliveryShippingParams();
$uploadParams[] = $storeDeliveryParams;
}
// 处理微信相关参数
return $this->setWechatParams($uploadParams, $wechatPay);
}
/**
* 修改物流是获取指定 包裹的 shippingParams
*
* @param \think\Model $express
* @return array
*/
public function getChangeShippingParams($express)
{
$wechatPay = $this->getWechatPay();
$this->setSendOrderItems();
$orderExpresses = collection([$express]); // 指定包裹
// 获取包裹的 params
$uploadParams = $this->getExpressShippingParamsByExpresses($orderExpresses);
// 处理微信相关参数
return $this->setWechatParams($uploadParams, $wechatPay);
}
/**
* 获取订单所有包裹的 shippingParams
*
* @return array
*/
private function getExpressShippingParams()
{
$orderExpresses = collection(OrderExpress::where('order_id', $this->order['id'])->select());
return $this->getExpressShippingParamsByExpresses($orderExpresses);
}
/**
* 获取订单指定包裹的 shippingParams
*
* @param \think\Model $order
* @param \think\Collection $orderExpresses
* @return array
*/
private function getExpressShippingParamsByExpresses($orderExpresses)
{
$uploadParams = [];
if (!$orderExpresses->isEmpty()) {
$orderAddress = OrderAddress::where('order_id', $this->order['id'])->find();
$receiver_contact = $orderAddress ? mb_substr($orderAddress->mobile, 0, 3) . '****' . mb_substr($orderAddress->mobile, -4) : '130****0000';
$shippingList = [];
foreach ($orderExpresses as $orderExpress) {
$currentItems = $this->getItemsByCondition('order_express_id', $orderExpress->id);
$item_desc = [];
foreach ($currentItems as $currentItem) {
$item_desc[] = $currentItem['goods_title'] . '*' . $currentItem['goods_num'];
}
$item_desc = join(', ', $item_desc);
$item_desc = mb_strlen($item_desc) > 110 ? mb_substr($item_desc, 0, 110) . ' 等商品' : $item_desc; // 处理字符串
$shippingList[] = [
'tracking_no' => $orderExpress['express_no'],
'express_company' => $orderExpress['express_code'],
'item_desc' => $item_desc,
'contact' => [
'receiver_contact' => $receiver_contact
]
];
}
if ($shippingList) {
// 发货
$uploadParams[] = [
'logistics_type' => $this->getLogisticsType('express'),
'shipping_list' => $shippingList,
];
}
}
return $uploadParams;
}
/**
* 获取订单中虚拟商品的 shippingParams
*
* @return array
*/
private function getVirtualShippingParams()
{
// 是否存在虚拟发货商品
$virtualItems = $this->getItemsByCondition('dispatch_type', ['autosend', 'custom'], 'in_array');
if (!$virtualItems->isEmpty()) {
$shippingList = [];
$item_desc = [];
foreach ($virtualItems as $virtualItem) {
$item_desc[] = $virtualItem['goods_title'] . '*' . $virtualItem['goods_num'];
}
$item_desc = join(', ', $item_desc);
$item_desc = mb_strlen($item_desc) > 110 ? mb_substr($item_desc, 0, 110) . ' 等商品' : $item_desc; // 处理字符串
$shippingList[] = [
'item_desc' => $item_desc,
];
// 发货
$currentParams = [
'logistics_type' => $this->getLogisticsType('autosend'),
'shipping_list' => $shippingList,
];
}
return $currentParams ?? null;
}
/**
* 获取订单中到店自提商品的 shippingParams
*
* @return array
*/
public function getSelfetchShippingParams()
{
// 到店自提商品
$selfetchItems = $this->getItemsByCondition('dispatch_type', ['selfetch'], 'in_array');
if (!$selfetchItems->isEmpty()) {
$shippingList = [];
$item_desc = [];
foreach ($selfetchItems as $selfetchItem) {
$item_desc[] = $selfetchItem['goods_title'] . '*' . $selfetchItem['goods_num'];
}
$item_desc = join(', ', $item_desc);
$item_desc = mb_strlen($item_desc) > 110 ? mb_substr($item_desc, 0, 110) . ' 等商品' : $item_desc; // 处理字符串
$shippingList[] = [
'item_desc' => $item_desc,
];
// 发货
$currentParams = [
'logistics_type' => $this->getLogisticsType('selfetch'),
'shipping_list' => $shippingList,
];
}
return $currentParams ?? null;
}
/**
* 获取订单中店铺配送商品的 shippingParams
*
* @return array
*/
public function getStoreDeliveryShippingParams()
{
// 到店自提商品
$storeDeliveryItems = $this->getItemsByCondition('dispatch_type', ['store_delivery'], 'in_array');
if (!$storeDeliveryItems->isEmpty()) {
$shippingList = [];
$item_desc = [];
foreach ($storeDeliveryItems as $storeDeliveryItem) {
$item_desc[] = $storeDeliveryItem['goods_title'] . '*' . $storeDeliveryItem['goods_num'];
}
$item_desc = join(', ', $item_desc);
$item_desc = mb_strlen($item_desc) > 110 ? mb_substr($item_desc, 0, 110) . ' 等商品' : $item_desc; // 处理字符串
$shippingList[] = [
'item_desc' => $item_desc,
];
// 发货
$currentParams = [
'logistics_type' => $this->getLogisticsType('store_delivery'),
'shipping_list' => $shippingList,
];
}
return $currentParams ?? null;
}
/**
* 设置 orderItems (这里是订单中的所有 items
*
* @return void
*/
private function setSendOrderItems()
{
$orderItems = OrderItem::where('order_id', $this->order['id'])->where('refund_status', OrderItem::REFUND_STATUS_NOREFUND)
->whereIn('dispatch_status', [OrderItem::DISPATCH_STATUS_SENDED, OrderItem::DISPATCH_STATUS_GETED])->select();
$this->orderItems = $orderItems instanceof \think\Collection ? $orderItems : collection($orderItems);
$this->dispatchTypes = array_values(array_unique(array_filter($this->orderItems->column('dispatch_type'))));
}
/**
* 根据条件获取指定 itemd
*
* @param string $field
* @param mixed $value
* @return \think\Collection
*/
private function getItemsByCondition($field, $value, $exp = '')
{
$new = [];
foreach ($this->orderItems as $item) {
if ($exp == 'in_array') {
if (in_array($item[$field], $value)) {
$new[] = $item;
}
} else {
if ($item[$field] == $value) {
$new[] = $item;
}
}
}
return collection($new);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace addons\shopro\service\order\shippingInfo;
class TradeOrderShippingInfo extends Base
{
/**
* 获取整个订单的 shippingParams 参数
*
* @return array
*/
public function getShippingParams()
{
$wechatPay = $this->getWechatPay('trade_order');
$uploadParams = [];
if ($this->order->type == 'recharge') {
// 充值订单
$virtualParams = $this->getVirtualShippingParams();
$uploadParams[] = $virtualParams;
}
// 处理微信相关参数
return $this->setWechatParams($uploadParams, $wechatPay);
}
/**
* 获取订单中虚拟商品的 shippingParams
*
* @return array
*/
private function getVirtualShippingParams()
{
$item_desc = '用户充值订单';
$shippingList[] = [
'item_desc' => $item_desc,
];
// 发货
return [
'logistics_type' => $this->getLogisticsType('autosend'),
'shipping_list' => $shippingList,
];
}
}

View File

@@ -0,0 +1,415 @@
<?php
namespace addons\shopro\service\pay;
use think\Log;
use app\admin\model\shopro\Pay as PayModel;
use app\admin\model\shopro\user\User;
use app\admin\model\shopro\order\Action;
use think\helper\Str;
use addons\shopro\service\Wallet as WalletService;
class PayOper
{
protected $user = null;
/**
* 实例化
*
* @param mixed $user
*/
public function __construct($user = null)
{
// 优先使用传入的用户
$this->user = $user ? (is_numeric($user) ? User::get($user) : $user) : auth_user();
}
/**
* 微信预付款
*
* @param think\Model $order
* @param float $money
* @param string $order_type
* @return think\Model
*/
public function wechat($order, $money, $order_type = 'order')
{
$pay = $this->addPay($order, [
'order_type' => $order_type,
'pay_type' => 'wechat',
'pay_fee' => $money,
'real_fee' => $money,
'transaction_id' => null,
'payment_json' => [],
'status' => PayModel::PAY_STATUS_UNPAID
]);
return $pay;
}
/**
* 支付宝预付款
*
* @param think\Model $order
* @param float $money
* @param string $order_type
* @return think\Model
*/
public function alipay($order, $money, $order_type = 'order')
{
$pay = $this->addPay($order, [
'order_type' => $order_type,
'pay_type' => 'alipay',
'pay_fee' => $money,
'real_fee' => $money,
'transaction_id' => null,
'payment_json' => [],
'status' => PayModel::PAY_STATUS_UNPAID
]);
return $pay;
}
/**
* 余额付款
*
* @param think\Model $order
* @param float $money
* @param string $order_type
* @return think\Model
*/
public function money($order, $money, $order_type = 'order')
{
// 余额支付金额,传入金额和剩余支付金额最大值
$money = $order->remain_pay_fee > $money ? $money : $order->remain_pay_fee; // 混合支付不能超过订单应支付总金额
// 扣除用户余额
WalletService::change($this->user, 'money', -$money, 'order_pay', [
'order_id' => $order->id,
'order_sn' => $order->order_sn,
'order_type' => $order_type,
]);
// 添加支付记录
$pay = $this->addPay($order, [
'order_type' => $order_type,
'pay_type' => 'money',
'pay_fee' => $money,
'real_fee' => $money,
'transaction_id' => null,
'payment_json' => [],
'status' => PayModel::PAY_STATUS_PAID
]);
// 余额直接支付成功,更新订单剩余应付款金额,并检测订单状态
return $this->checkAndPaid($order, $order_type);
}
/**
* 积分支付
*
* @param think\Model $order
* @param float $money
* @param string $order_type
* @return think\Model
*/
public function score($order, $score, $order_type = 'order')
{
if ($order_type == 'order') {
if ($order['type'] == 'score') {
$log_type = 'score_shop_pay';
$real_fee = $score; // 积分商城真实抵扣,就是积分
} else {
$log_type = 'order_pay';
// $real_fee = ; // 积分商城真实抵扣,就是积分
error_stop('缺少积分抵扣金额'); // 支持积分抵扣时补全
}
}
WalletService::change($this->user, 'score', -$score, $log_type, [
'order_id' => $order->id,
'order_sn' => $order->order_sn,
'order_type' => $order_type,
]);
// 添加支付记录
$pay = $this->addPay($order, [
'order_type' => $order_type,
'pay_type' => 'score',
'pay_fee' => $score,
'real_fee' => $real_fee,
'transaction_id' => null,
'payment_json' => [],
'status' => PayModel::PAY_STATUS_PAID
]);
// 积分直接支付成功,更新订单剩余应付款金额,并检测订单状态
return $this->checkAndPaid($order, $order_type);
}
/**
* 线下支付(货到付款)
*
* @param think\Model $order
* @param float $money
* @param string $order_type
* @return think\Model
*/
public function offline($order, $money, $order_type = 'order')
{
// 添加支付记录
$pay = $this->addPay($order, [
'order_type' => $order_type,
'pay_type' => 'offline',
'pay_fee' => $money,
'real_fee' => $money,
'transaction_id' => null,
'payment_json' => [],
'status' => PayModel::PAY_STATUS_PAID
]);
// 更新订单剩余应付款金额,并检测订单状态
return $this->checkAndPaid($order, $order_type, 'offline');
}
/**
* 微信支付宝支付回调通用方法
*
* @param \think\Model $pay
* @param array $notify
* @return void
*/
public function notify($pay, $notify)
{
$pay->status = PayModel::PAY_STATUS_PAID;
$pay->transaction_id = $notify['transaction_id'];
$pay->buyer_info = $notify['buyer_info'];
$pay->payment_json = $notify['payment_json'];
$pay->paid_time = time();
$pay->save();
$orderModel = $this->getOrderModel($pay->order_type);
$order = new $orderModel();
$order = $order->where('id', $pay->order_id)->find();
if (!$order) {
// 订单未找到,非正常情况,这里记录日志
Log::write('pay-notify-error:order notfound;pay:' . json_encode($pay) . ';notify:' . json_encode($notify));
return false;
}
if ($order->status == $order::STATUS_UNPAID) { // 未支付,检测支付状态
$order = $this->checkAndPaid($order, $pay->order_type);
}
return $order;
}
/**
* 更新订单剩余应支付金额,并且检测订单状态
*
* @param think\Model $order
* @param string $order_type
* @return think\Model
*/
public function checkAndPaid($order, $order_type, $pay_mode = 'online')
{
// 获取订单已支付金额
$payed_fee = $this->getPayedFee($order, $order_type);
$remain_pay_fee = bcsub($order->pay_fee, (string)$payed_fee, 2);
$order->remain_pay_fee = $remain_pay_fee;
if ($remain_pay_fee <= 0) {
$order->remain_pay_fee = 0;
$order->paid_time = time();
$order->status = $order::STATUS_PAID;
} else {
if ($pay_mode == 'offline') {
// 订单未支付成功,并且是线下支付(货到付款),将订单状态改为 pending
$order->status = $order::STATUS_PENDING;
$order->ext = array_merge($order->ext, ['pending_time' => time()]); // 货到付款下单时间
$order->pay_mode = 'offline';
}
}
$order->save();
if ($order->status == $order::STATUS_PAID) {
// 订单支付完成
$user = User::where('id', $order->user_id)->find();
if ($order_type == 'order') {
if ($pay_mode == 'offline') {
Action::add($order, null, auth_admin(), 'admin', '管理员操作自动货到付款支付成功');
// 在控制器执行后续内容,这里不再处理
return $order;
} else {
Action::add($order, null, $user, 'user', '用户支付成功');
// 支付成功后续使用异步队列处理
\think\Queue::push('\addons\shopro\job\OrderPaid@paid', ['order' => $order, 'user' => $user], 'shopro-high');
}
} else if ($order_type == 'trade_order') {
// 支付成功后续使用异步队列处理
\think\Queue::push('\addons\shopro\job\trade\OrderPaid@paid', ['order' => $order, 'user' => $user], 'shopro-high');
}
} else if ($order->status == $order::STATUS_PENDING) {
// 货到付款,添加货到付款队列(后续也需要处理拼团, 减库存等等)
$user = User::where('id', $order->user_id)->find();
if ($order_type == 'order') {
Action::add($order, null, $user, 'user', '用户货到付款下单成功');
// 支付成功后续使用异步队列处理
\think\Queue::push('\addons\shopro\job\OrderPaid@offline', ['order' => $order, 'user' => $user], 'shopro-high');
}
}
return $order;
}
/**
* 获取订单已支付金额,商城订单 计算 积分抵扣金额
*
* @param \think\Model $order
* @param string $order_type
* @return string
*/
public function getPayedFee($order, $order_type)
{
// 锁定读取所有已支付的记录,判断已支付金额
$pays = PayModel::{'type' . Str::studly($order_type)}()->paid()->where('order_id', $order->id)->lock(true)->select();
// 商城或者积分商城订单
$payed_fee = '0';
foreach ($pays as $key => $pay) {
if ($pay->pay_type == 'score') {
if ($order_type == 'order' && $order['type'] == 'goods') {
// 商城类型订单,并且不是积分商城订单,加上积分抵扣真实金额
$payed_fee = bcadd($payed_fee, $pay->real_fee, 2);
} else {
// 其他类型,需要计算积分抵扣的金额时
}
} else {
$payed_fee = bcadd($payed_fee, $pay->real_fee, 2);
}
}
return $payed_fee;
}
/**
* 获取剩余可退款的pays 记录(不含积分抵扣)
*
* @param integer $order_id
* @param string $sort 排序money=优先退回余额支付的钱
* @return \think\Collection
*/
public function getCanRefundPays($order_id, $sort = 'money')
{
// 商城订单,已支付的 pay 记录, 这里只查 钱的支付记录,不查积分
$pays = PayModel::typeOrder()->paid()->isMoney()->where('order_id', $order_id)->lock(true)->order('id', 'asc')->select();
$pays = collection($pays);
if ($sort == 'money') {
// 对 pays 进行排序,优先退 money 的钱
$pays = $pays->sort(function ($a, $b) {
if ($a['pay_type'] == 'money' && $b['pay_type'] == 'money') {
return 0;
} else if ($a['pay_type'] == 'money' && $b['pay_type'] != 'money') {
return -1;
} else if ($a['pay_type'] != 'money' && $b['pay_type'] == 'money') {
return 1;
} else {
return 0;
}
});
$pays = $pays->values();
}
return $pays;
}
/**
* 获取剩余可退款金额,不含积分相关支付
*
* @param mixed $pays
* @return string
*/
public function getRemainRefundMoney($pays)
{
// 拿到 所有可退款的支付记录
$pays = ($pays instanceof \think\Collection) ? $pays : $this->getCanRefundPays($pays);
// 支付金额,除了已经退完款的金额 (这里不退积分)
$payed_money = (string)array_sum($pays->column('pay_fee'));
// 已经退款金额 (这里不退积分)
$refunded_money = (string)array_sum($pays->column('refund_fee'));
// 当前剩余的最大可退款金额,支付金额 - 已退款金额
$remain_max_refund_money = bcsub($payed_money, $refunded_money, 2);
return $remain_max_refund_money;
}
/**
* 添加 pay 记录
*
* @param think\Model $order
* @param array $params
* @return think\Model
*/
public function addPay($order, $params)
{
$payModel = new PayModel();
$payModel->order_type = $params['order_type'];
$payModel->order_id = $order->id;
$payModel->pay_sn = get_sn($this->user->id, 'P');
$payModel->user_id = $this->user->id;
$payModel->pay_type = $params['pay_type'];
$payModel->pay_fee = $params['pay_fee'];
$payModel->real_fee = $params['real_fee'];
$payModel->transaction_id = $params['transaction_id'];
$payModel->payment_json = $params['payment_json'];
$payModel->paid_time = $params['status'] == PayModel::PAY_STATUS_PAID ? time() : null;
$payModel->status = $params['status'];
$payModel->refund_fee = 0;
$payModel->save();
return $payModel;
}
public function getOrderModel($order_type)
{
switch ($order_type) {
case 'trade_order':
$orderModel = '\\app\\admin\\model\\shopro\\trade\\Order';
break;
case 'order':
$orderModel = '\\app\\admin\\model\\shopro\\order\\Order';
break;
default:
$orderModel = '\\app\\admin\\model\\shopro\\order\\Order';
break;
}
return $orderModel;
}
}

View File

@@ -0,0 +1,352 @@
<?php
namespace addons\shopro\service\pay;
use think\Log;
use think\Db;
use addons\shopro\library\pay\PayService;
use app\admin\model\shopro\Pay as PayModel;
use app\admin\model\shopro\user\User;
use app\admin\model\shopro\Refund as RefundModel;
use addons\shopro\service\Wallet as WalletService;
class PayRefund
{
protected $user = null;
/**
* 实例化
*
* @param mixed $user
*/
public function __construct($user = null)
{
// 优先使用传入的用户
$this->user = is_numeric($user) ? User::get($user) : $user;
}
/**
* 之前没有经历过任何的退款(刚支付的订单要退款|拼团失败订单等)
*
* @param \think\Model $pay
* @param string $refund_type
* @return void
*/
public function fullRefund($pay, $data = [])
{
$pay->refund_fee = $pay->pay_fee;
$pay->status = PayModel::PAY_STATUS_REFUND; // 直接退款完成
$pay->save();
// 添加退款单
$refund = $this->add($pay, $pay->pay_fee, $data);
// 退款
$refund = $this->return($pay, $refund);
return $refund;
}
/**
* 部分退款,指定退款金额,并且检测 pay 是否已经退款完成
*
* @param \think\Model $pay
* @param float $refund_money
* @param array $data
* @return \think\Model
*/
public function refund($pay, $refund_money, $data = [])
{
$pay->refund_fee = Db::raw('refund_fee + ' . $refund_money);
$pay->save();
// 添加退款单
$refund = $this->add($pay, $refund_money, $data);
// 退款
$refund = $this->return($pay, $refund);
// 检查 pay 是否退款完成,
$this->checkPayAndRefunded($pay);
return $refund;
}
/**
* 微信支付宝退款回调方法
*
* @param array $data
* @return void
*/
public function notify($data)
{
$out_trade_no = $data['out_trade_no'];
$out_refund_no = $data['out_refund_no'];
$payment_json = $data['payment_json'];
$pay = PayModel::where('pay_sn', $out_trade_no)->find();
if (!$pay) {
Log::write('refund-notify-error:paymodel notfound;pay:' . json_encode($data));
return false;
}
if ($pay->order_type == 'order') {
$refund = RefundModel::where('refund_sn', $out_refund_no)->find();
if (!$refund || $refund->status != RefundModel::STATUS_ING) {
// 退款单不存在,或者已退款
return false;
}
$refund = $this->completed($refund, $payment_json);
return true;
} else {
// 如有其他订单类型如果支持退款,逻辑这里补充
}
}
/**
* 退回
*
* @param \think\Model $pay
* @param \think\Model $refund
* @return \think\Model
*/
protected function return($pay, $refund)
{
$method = $refund->refund_method;
if (method_exists($this, $method)) {
$refund = $this->{$method}($pay, $refund);
} else {
error_stop('退款方式不支持');
}
return $refund;
}
/**
* 退余额
*
* @param \think\Model $pay
* @param \think\Model $refund
* @return \think\Model
*/
private function money($pay, $refund)
{
// 退回用户余额
WalletService::change($pay->user_id, 'money', $refund->refund_fee, 'order_refund', [
'refund_id' => $refund->id,
'refund_sn' => $refund->refund_sn,
'pay_id' => $pay->id,
'pay_sn' => $pay->pay_sn,
'order_id' => $pay->order_id,
'order_type' => $pay->order_type,
]);
$refund = $this->completed($refund);
return $refund;
}
/**
* 退积分
*
* @param \think\Model $pay
* @param \think\Model $refund
* @return \think\Model
*/
private function score($pay, $refund)
{
// 退回用户积分
WalletService::change($pay->user_id, 'score', $refund->refund_fee, 'order_refund', [
'refund_id' => $refund->id,
'refund_sn' => $refund->refund_sn,
'pay_id' => $pay->id,
'pay_sn' => $pay->pay_sn,
'order_id' => $pay->order_id,
'order_type' => $pay->order_type,
]);
$refund = $this->completed($refund);
return $refund;
}
/**
* 退 offline
*
* @param \think\Model $pay
* @param \think\Model $refund
* @return \think\Model
*/
private function offline($pay, $refund)
{
// offline 退款啥也不干,钱还是线下退回,线上不处理
$refund = $this->completed($refund);
return $refund;
}
/**
* 退微信
*
* @param \think\Model $pay
* @param \think\Model $refund
* @return \think\Model
*/
private function wechat($pay, $refund)
{
$order_data = [
'out_trade_no' => $pay->pay_sn,
'out_refund_no' => $refund->refund_sn,
'reason' => $refund->remark,
'amount' => [
'refund' => $refund->refund_fee,
'total' => $pay->pay_fee,
'currency' => 'CNY'
]
];
$pay = new PayService('wechat', $refund->platform);
$result = $pay->refund($order_data);
// 微信通知回调 pay->notifyRefund
if (isset($result['status']) && in_array($result['status'], ['SUCCESS', 'PROCESSING'])) {
// 微信返回的状态会是 PROCESSING
return true;
} else {
error_stop('退款失败:' . (isset($result['message']) ? $result['message'] : json_encode($result, JSON_UNESCAPED_UNICODE)));
}
return $refund;
}
/**
* 退支付宝
*
* @param \think\Model $pay
* @param \think\Model $refund
* @return \think\Model
*/
private function alipay($pay, $refund)
{
$order_data = [
'out_trade_no' => $pay->pay_sn,
'out_request_no' => $refund->refund_sn,
'refund_amount' => $refund->refund_fee,
'refund_reason' => $refund->remark
];
$pay = new PayService('alipay', $refund->platform);
$result = $pay->refund($order_data);
// 支付宝通知回调 pay->notify // 是和支付通知一个地址
if ($result['code'] == "10000") {
return true;
} else {
error_stop('退款失败:' . $result['msg'] . (isset($result["sub_msg"]) && $result['sub_msg'] ? '-' . $result['sub_msg'] : ''));
}
return $refund;
}
/**
* 添加 pay 记录
*
* @param think\Model $pay
* @param float $refund_money
* @param array $data
* @return think\Model
*/
private function add($pay, $refund_money, $data = [])
{
$refund_type = $data['refund_type'] ?? 'back';
// 判断退款方式
if ($refund_type == 'back') {
// 原路退回
$refund_method = $pay->pay_type;
} else {
if ($pay->pay_type == 'score') {
// 退积分
$refund_method = 'score';
} else if ($pay->pay_type == 'offline') {
// 退积分
$refund_method = 'offline';
} else {
// 退回到余额
$refund_method = 'money';
}
}
$refund = new RefundModel();
$refund->refund_sn = get_sn($this->user->id, 'R');
$refund->order_id = $pay->order_id;
$refund->pay_id = $pay->id;
$refund->pay_type = $pay->pay_type;
$refund->refund_fee = $refund_money;
$refund->refund_type = $refund_type;
$refund->refund_method = $refund_method;
$refund->status = RefundModel::STATUS_ING;
$refund->platform = $data['platform'] ?? null;
$refund->remark = $data['remark'] ?? null;
$refund->save();
return $refund;
}
/**
* 完成退款单
*
* @param \think\Model $refund
* @return \think\Model
*/
private function completed($refund, $payment_json = '')
{
$refund->status = RefundModel::STATUS_COMPLETED;
$refund->payment_json = $payment_json;
$refund->save();
return $refund;
}
/**
* 检查 pay 并且完成退款
*
* @param \think\Model $pay
* @return void
*/
private function checkPayAndRefunded($pay)
{
$pay = PayModel::where('id', $pay->id)->find();
if ($pay->refund_fee >= $pay->pay_fee) {
// 退款完成了
$pay->status = PayModel::PAY_STATUS_REFUND;
$pay->save();
}
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace addons\shopro\service\third\apple;
use app\common\library\Auth;
use addons\shopro\service\user\UserAuth;
use app\admin\model\shopro\ThirdOauth;
class Apple
{
public function __construct()
{
}
/**
* AppleId登陆
*
* @return array
*/
public function login($payload)
{
$identityToken = $payload['identityToken'];
$openId = $payload['openId'];
$appleSignInPayload = \AppleSignIn\ASDecoder::getAppleSignInPayload($identityToken);
$isValid = $appleSignInPayload->verifyUser($openId);
if (!$isValid) return false;
$nickname = '';
if (!empty($payload['fullName'])) {
$hasFamilyName = !empty($payload['fullName']['familyName']);
$nickname = ($hasFamilyName ? $payload['fullName']['familyName'] : '') . ($hasFamilyName ? ' ' : '') . $payload['fullName']['giveName'];
}
$appleUser = [
'openid' => $openId,
'nickname' => $nickname,
'avatar' => ''
];
$oauthInfo = $this->createOrUpdateOauthInfo($appleUser);
if (!$oauthInfo->user_id) {
$this->registerOrBindUser($oauthInfo);
}
$auth = Auth::instance();
$ret = $auth->direct($oauthInfo->user_id);
if ($ret) {
$oauthInfo->login_num += 1;
set_token_in_header($auth->getToken());
return true;
} else {
$oauthInfo->user_id = 0;
$oauthInfo->save();
return false;
}
}
/**
* 创建/更新用户认证信息
*
* @return think\Model
*/
private function createOrUpdateOauthInfo($appleUser)
{
$oauthInfo = ThirdOauth::where([
'openid' => $appleUser['openid'],
'provider' => 'apple',
'platform' => 'App'
])->find();
if (!$oauthInfo) { // 创建新的third_oauth条目
$appleUser['provider'] = 'apple';
$appleUser['platform'] = 'App';
ThirdOauth::create($appleUser);
$oauthInfo = ThirdOauth::where([
'openid' => $appleUser['openid'],
'provider' => 'apple',
'platform' => 'App'
])->find();
}
if ($oauthInfo) { // 更新授权信息
$oauthInfo->save($appleUser);
}
return $oauthInfo;
}
/**
* 注册/绑定用户
*
* @return think\Model
*/
private function registerOrBindUser($oauthInfo, $user_id = 0)
{
if ($oauthInfo->user_id) {
error_stop('该账号已绑定其他用户');
}
if ($user_id === 0) { // 注册新用户
$user = (new UserAuth())->register([
'nickname' => $oauthInfo->nickname,
'avatar' => $oauthInfo->avatar,
]);
$user_id = $user->id;
}
$oauthInfo->user_id = $user_id;
return $oauthInfo->save();
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace addons\shopro\service\third\wechat;
use addons\shopro\facade\Wechat;
use fast\Random;
use app\common\library\Auth;
use app\admin\model\shopro\ThirdOauth;
class MiniProgram
{
public $wechat;
protected $request;
protected $payload;
public function __construct($payload = [])
{
$this->payload = $payload;
$this->wechat = Wechat::miniProgram();
}
// 小程序登录
public function login()
{
// https://developers.weixin.qq.com/community/develop/doc/00022c683e8a80b29bed2142b56c01
if (empty($this->payload['sessionId'])) {
error_stop('未获取到登陆态, 请重试', -1);
}
$sessionData = redis_cache($this->payload['sessionId']);
if (empty($sessionData)) {
error_stop('登陆态已过期, 请重试', -1);
}
$wechatUser = [
'openid' => $sessionData['openid'],
'unionid' => $sessionData['unionid'] ?? '',
'mobile' => '',
'avatar' => '',
'nickname' => '',
];
return $wechatUser;
}
public function bind()
{
if (empty($this->payload['sessionId'])) {
error_stop('未获取到登陆态, 请重试', -1);
}
$sessionData = redis_cache($this->payload['sessionId']);
if (empty($sessionData)) {
error_stop('登陆态已过期, 请重试', -1);
}
$wechatUser = [
'openid' => $sessionData['openid'],
'unionid' => $sessionData['unionid'] ?? '',
'avatar' => '',
'nickname' => '',
];
return $wechatUser;
}
// 解密微信小程序手机号
public function getUserPhoneNumber()
{
if (empty($this->payload['sessionId'])) {
error_stop('未获取到登陆态, 请重试', -1);
}
$sessionData = redis_cache($this->payload['sessionId']);
if (empty($sessionData)) {
error_stop('登陆态已过期, 请重试', -1);
}
$phoneInfo = $this->wechat->encryptor->decryptData($sessionData['session_key'], $this->payload['iv'], $this->payload['encryptedData']);
if (empty($phoneInfo['purePhoneNumber'])) {
error_stop('获取失败,请重试');
}
if ($phoneInfo['countryCode'] !== '86') {
error_stop('仅支持大陆地区手机号');
}
return $phoneInfo['purePhoneNumber'];
}
/**
* 获取session_id, 缓存 session_key, openid (unionid), 自动登录
*
* @return string
*/
public function getSessionId()
{
if (empty($this->payload['code'])) {
error_stop('缺少code参数');
}
$decryptData = $this->wechat->auth->session($this->payload['code']);
if(!empty($decryptData['errmsg'])) {
error_stop($decryptData['errmsg']);
}
if (empty($decryptData['session_key'])) {
error_stop('未获取到登陆态, 请重试', -1);
}
$auto_login = $this->payload['auto_login'] ?? false;
// 自动登录流程
if($auto_login) {
$oauthInfo = ThirdOauth::getByOpenid($decryptData['openid']);
if($oauthInfo && $oauthInfo->user_id) {
$auth = Auth::instance();
$ret = $auth->direct($oauthInfo->user_id);
if ($ret) {
set_token_in_header($auth->getToken());
}
}
}
$session_id = Random::uuid();
redis_cache($session_id, $decryptData, 60 * 60 * 24 * 7); // session_key缓存保留一周
return ['session_id' => $session_id, 'auto_login' => $auto_login];
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace addons\shopro\service\third\wechat;
use addons\shopro\facade\Wechat;
class OfficialAccount
{
public $wechat;
protected $request;
protected $payload;
public function __construct($payload)
{
$this->payload = $payload;
$this->request = request();
$this->wechat = Wechat::officialAccount();
}
public function login()
{
$code = $this->request->get('code');
if (empty($code)) {
error_stop('缺少code参数');
}
$decryptData = $this->wechat->oauth->user()->getOriginal();
$wechatUser = [
'openid' => $decryptData['openid'],
'unionid' => $decryptData['unionid'] ?? '',
'avatar' => $decryptData['headimgurl'],
'nickname' => $decryptData['nickname'],
];
return $wechatUser;
}
public function bind()
{
return $this->login();
}
/**
* 获取网页登录地址redirect+返回code
*
* @return string
*/
public function oauthLogin()
{
// 返回前端
if (!empty($this->request->param('code'))) {
if($this->payload['event'] === 'bind') {
$query['bind_code'] = $this->request->param('code');
}else {
$query['login_code'] = $this->request->param('code');
}
return [
'redirect_url' => $this->payload['page'] . '?' . http_build_query($query)
];
} else {
$query = [
'platform' => 'officialAccount',
'payload' => urlencode(json_encode($this->payload))
];
$loginUrl = $this->request->domain() . '/addons/shopro/third.wechat/oauthLogin?' . http_build_query($query);
return [
'login_url' => $this->wechat->oauth->scopes(['snsapi_userinfo'])->redirect($loginUrl)->getTargetUrl()
];
}
}
public function jssdk($APIs)
{
$this->wechat->jssdk->setUrl($this->payload['url']);
return $this->wechat->jssdk->buildConfig($APIs, false, false, false);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace addons\shopro\service\third\wechat;
use fast\Http;
use addons\shopro\facade\Wechat;
class OpenPlatform
{
public $wechat;
protected $request;
protected $payload;
public function __construct($payload)
{
$this->payload = $payload;
$this->request = request();
$this->wechat = Wechat::openPlatform();
}
public function login()
{
$payload = $this->payload;
if (empty($payload['code'])) {
error_stop('登陆失败');
}
$config = sheep_config('shop.platform.App');
// 获取accessToken & openid
$res = Http::get('https://api.weixin.qq.com/sns/oauth2/access_token', [
'appid' => $config['app_id'],
'secret' => $config['secret'],
'code' => $payload['code'],
'grant_type' => 'authorization_code'
]);
$decryptedData = json_decode($res, true);
if (isset($decryptedData['errmsg'])) {
error_stop($decryptedData['errmsg']);
}
// 获取userInfo
$res = Http::get('https://api.weixin.qq.com/sns/userinfo', ['access_token' => $decryptedData['access_token'], 'openid' => $decryptedData['openid']]);
$userInfo = is_string($res) ? json_decode($res, true) : $res;
if (isset($userInfo['errmsg'])) {
error_stop($userInfo['errmsg']);
}
$wechatUser = [
'openid' => $userInfo['openid'],
'unionid' => $userInfo['unionid'] ?? '',
'avatar' => $userInfo['headimgurl'],
'nickname' => $userInfo['nickname'],
];
return $wechatUser;
}
public function bind()
{
return $this->login();
}
}

View File

@@ -0,0 +1,210 @@
<?php
namespace addons\shopro\service\third\wechat;
use app\common\library\Auth;
use addons\shopro\service\user\UserAuth;
use app\admin\model\shopro\ThirdOauth;
use app\admin\model\shopro\user\User as UserModel;
class Wechat
{
// 平台
protected $platform;
// 转发服务
protected $service;
public function __construct($platform, $payload = [])
{
$this->platform = $platform;
$this->service = $this->setPlatformService($payload);
}
public function getApp()
{
return $this->service->wechat;
}
/**
* 微信登陆
*
* @return array
*/
public function login()
{
$wechatUser = $this->service->login();
$oauthInfo = $this->createOrUpdateOauthInfo($wechatUser);
$this->registerOrBindUser($oauthInfo, 0, ['mobile' => $wechatUser['mobile'] ?? '']);
$oauthInfo->save();
$auth = Auth::instance();
$ret = $auth->direct($oauthInfo->user_id);
if ($ret) {
$oauthInfo->login_num += 1;
set_token_in_header($auth->getToken());
return true;
} else {
$oauthInfo->user_id = 0;
$oauthInfo->save();
return false;
}
}
/**
* 微信绑定
*
* @return array
*/
public function bind(Object $user)
{
$wechatUser = $this->service->bind();
$oauthInfo = $this->createOrUpdateOauthInfo($wechatUser);
if ($this->registerOrBindUser($oauthInfo, $user->id)) {
return true;
}
return false;
}
/**
* 微信解绑
*
* @return array
*/
public function unbind()
{
$user = auth_user();
if (!$user->verification->mobile) {
error_stop('请先绑定手机号后再进行操作');
}
$oauthInfo = ThirdOauth::where([
'user_id' => $user->id,
'platform' => $this->platform,
'provider' => 'wechat'
])->find();
if ($oauthInfo) {
$oauthInfo->delete();
return true;
}
return false;
}
/**
* 创建/更新用户认证信息
*
* @return think\Model
*/
private function createOrUpdateOauthInfo($wechatUser, $extend = [])
{
$oauthInfo = ThirdOauth::getByOpenid($wechatUser['openid']);
if (!$oauthInfo) { // 创建新的third_oauth条目
$wechatUser['user_id'] = 0;
if (!empty($wechatUser['unionid'])) { // unionid账号合并策略
$unionOauthInfo = ThirdOauth::getByUnionid($wechatUser['unionid']);
if ($unionOauthInfo) {
$wechatUser['user_id'] = $unionOauthInfo->user_id;
}
}
$wechatUser['provider'] = 'wechat';
$wechatUser['platform'] = $this->platform;
$wechatUser = array_merge($wechatUser, $extend);
$thirdOauth = new ThirdOauth($wechatUser);
$thirdOauth->allowField(true)->save();
$oauthInfo = ThirdOauth::getByOpenid($wechatUser['openid']);
} else { // 更新授权信息
if ($this->platform === 'miniProgram') {
unset($wechatUser['avatar']);
unset($wechatUser['nickname']);
}
$oauthInfo->allowField(true)->save($wechatUser);
}
return $oauthInfo;
}
/**
* 注册/绑定用户
*
* @return think\Model
*/
private function registerOrBindUser($oauthInfo, $user_id = 0, $extend = [])
{
// 检查用户存在
if ($oauthInfo->user_id) {
$user = UserModel::get($oauthInfo->user_id);
if ($user && $user_id > 0) {
error_stop('该微信账号已绑定其他用户');
}
// 用户被删除,则重置第三方信息所绑定的用户id
if (!$user) {
$oauthInfo->user_id = 0;
}
}
// 绑定用户
if ($oauthInfo->user_id === 0 && $user_id > 0) {
$user = UserModel::get($user_id);
if ($user) {
$oauthInfo->user_id = $user_id;
return $oauthInfo->save();
} else {
error_stop('该用户暂不可绑定微信账号');
}
return false;
}
// 注册新用户
if ($oauthInfo->user_id === 0 && $user_id === 0) {
$auth = (new UserAuth())->register([
'nickname' => $oauthInfo->nickname ?? '',
'avatar' => $oauthInfo->avatar ?? '',
'mobile' => $extend['mobile'] ?? ''
]);
$user = $auth->getUser();
$oauthInfo->user_id = $user->id;
return $oauthInfo->save();
}
}
/**
* 设置平台服务类
*
* @return class
*/
private function setPlatformService($payload)
{
switch ($this->platform) {
case 'officialAccount':
$service = new OfficialAccount($payload);
break;
case 'miniProgram':
$service = new MiniProgram($payload);
break;
case 'openPlatform':
$service = new OpenPlatform($payload);
break;
}
if (!isset($service)) error_stop('平台参数有误');
return $service;
}
/**
* 方法转发到驱动提供者
*
* @param string $funcname
* @param array $arguments
* @return void
*/
public function __call($funcname, $arguments)
{
return $this->service->{$funcname}(...$arguments);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace addons\shopro\service\user;
use app\admin\model\shopro\user\User as UserModel;
class User
{
/**
* @name 追加消费金额
* @param int|object $user 会员对象或会员ID
* @param float $amount 变更金额
* @return boolean
*/
public static function consume($user, $amount)
{
// 判断金额
if ($amount == 0) {
return false;
}
// 判断用户
if (is_numeric($user)) {
$user = UserModel::getById($user);
}
if (!$user) {
error_stop('未找到用户');
}
// 更新会员余额信息
$user->setInc('total_consume', $amount);
return true;
}
}

View File

@@ -0,0 +1,233 @@
<?php
namespace addons\shopro\service\user;
use fast\Random;
use app\common\library\Auth;
use app\admin\model\shopro\user\User as UserModel;
class UserAuth
{
/**
* 认证用户
*
* @var object|null
*/
protected $auth = null;
public function __construct()
{
$this->auth = Auth::instance();
}
/**
* 用户注册
*
* @param array $params 注册信息
* @param array $params 至少包含 mobile 和 email 中的一个
* @return object|array
*/
public function register($params)
{
$verification = [];
if(!empty($params['username'])) {
$username = $params['username'];
$verification['username'] = 1;
}else {
$username = Random::alnum(8);
}
if(!empty($params['mobile'])) {
$mobile = $params['mobile'];
$verification['mobile'] = 1;
}else {
$mobile = '';
}
if(!empty($params['email'])) {
$email = $params['email'];
$verification['email'] = 1;
}else {
$email = '';
}
if(!empty($params['password'])) {
$password = $params['password'];
$verification['password'] = 1;
}else {
$password = Random::alnum(8);
}
if ($username || $mobile || $email) {
$user = UserModel::where(function ($query) use ($mobile, $email, $username) {
if ($mobile) {
$query->whereOr('mobile', $mobile);
}
if ($email) {
$query->whereOr('email', $email);
}
if ($username) {
$query->whereOr('username', $username);
}
})->find();
if ($user) {
error_stop('账号已注册,请直接登录');
}
}
$userDefaultConfig = $this->getUserDefaultConfig();
$extend = [
'avatar' => !empty($params['avatar']) ? $params['avatar'] : $userDefaultConfig['avatar'],
'nickname' => !empty($params['nickname']) ? $params['nickname'] : $userDefaultConfig['nickname'] . $username,
'group_id' => $userDefaultConfig['group_id'] ?? 1
];
$ret = $this->auth->register($username, $password, $email, $mobile, $extend);
if ($ret) {
$user = $this->auth->getUser();
$user->verification = $verification;
$user->save();
$hookData = ['user' => $user];
\think\Hook::listen('user_register_after', $hookData);
return $this->auth;
} else {
error_stop($this->auth->getError());
}
}
/**
* 重置密码
*
* @param array $params 至少包含 mobile 和 email 中的一个
* @return boolean
*/
public function resetPassword($params)
{
$mobile = $params['mobile'] ?? null;
$email = $params['email'] ?? null;
$password = $params['password'] ?? null;
if (!$params['mobile'] && !$params['email']) {
error_stop('参数错误');
}
$user = UserModel::where(function ($query) use ($mobile, $email) {
if ($mobile) {
$query->whereOr('mobile', $mobile);
}
if ($email) {
$query->whereOr('email', $email);
}
})->find();
if (!$user) {
error_stop(__('User not found'));
}
$this->auth->direct($user->id);
$ret = $this->auth->changepwd($password, '', true);
if(!$ret) {
error_stop($this->auth->getError());
}
if ($ret) {
$user = $this->auth->getUser();
$verification = $user->verification;
$verification->password = 1;
$user->verification = $verification;
$user->save();
}
return $ret;
}
/**
* 修改密码
*
* @param string $old_password
* @param string $password
* @return boolean
*/
public function changePassword($new_password, $old_password)
{
$ret = $this->auth->changepwd($new_password, $old_password);
if(!$ret) {
error_stop($this->auth->getError());
}
return $ret;
}
/**
* 修改手机号
* @param array $params
* @return bool
*/
public function changeMobile($params)
{
$user = auth_user();
$verification = $user->verification;
$verification->mobile = 1;
$user->verification = $verification;
$user->mobile = $params['mobile'];
$user->save();
return true;
}
/**
* 修改用户名
* @param array $params
* @return bool
*/
public function changeUsername($params)
{
$user = auth_user();
$verification = $user->verification;
$verification->username = 1;
$user->verification = $verification;
$user->username = $params['username'];
$user->save();
return true;
}
/**
* 退出登录
*/
public function logout()
{
$this->auth->logout();
}
/**
* 注销用户
*/
public function logoff()
{
$user = auth_user();
$user = UserModel::get($user->id);
$user->delete();
$this->logout();
}
/**
* 获取用户默认值配置
*
* @return object|array
*/
private function getUserDefaultConfig()
{
$config = sheep_config('shop.user');
return $config;
}
}