// +---------------------------------------------------------------------- namespace app\services\user; use app\dao\user\UserMoneyDao; use app\dao\user\UserDao; use crmeb\basic\BaseService; use app\services\user\MemberOrderServices; use app\dao\order\OrderCenterDao; use crmeb\exceptions\AdminException; use crmeb\exceptions\ApiException; class UserMoneyServices extends BaseService { /** * @var UserMoneyDao */ protected $dao; /** * 用户记录模板 * @var array[] */ protected $incomeData = [ 'pay_product' => [ 'title' => '余额支付购买商品', 'type' => 'pay_product', 'mark' => '余额支付{%num%}元购买商品', 'status' => 1, 'pm' => 0 ], 'pay_member' => [ 'title' => '余额支付购买会员', 'type' => 'pay_member', 'mark' => '余额支付{%num%}元购买会员', 'status' => 1, 'pm' => 0 ], 'offline_scan' => [ 'title' => '余额支付线下收银', 'type' => 'offline_scan', 'mark' => '余额支付{%num%}元线下收银', 'status' => 1, 'pm' => 0 ], 'pay_product_refund' => [ 'title' => '商品退款', 'type' => 'pay_product_refund', 'mark' => '订单退款到余额{%num%}元', 'status' => 1, 'pm' => 1 ], 'system_add' => [ 'title' => '系统增加余额', 'type' => 'system_add', 'mark' => '系统增加{%num%}余额', 'status' => 1, 'pm' => 1 ], 'system_sub' => [ 'title' => '系统减少余额', 'type' => 'system_sub', 'mark' => '系统扣除{%num%}余额', 'status' => 1, 'pm' => 0 ], 'user_recharge' => [ 'title' => '用户充值余额', 'type' => 'recharge', 'mark' => '成功充值余额{%price%}元,赠送{%give_price%}元', 'status' => 1, 'pm' => 1 ], 'user_recharge_refund' => [ 'title' => '用户充值退款', 'type' => 'recharge_refund', 'mark' => '退款扣除余额{%num%}元', 'status' => 1, 'pm' => 0 ], 'user_recharge_refund_release' => [ 'title' => '充值退款失败退回余额', 'type' => 'recharge_refund_release', 'mark' => '充值退款失败退回{%num%}元', 'status' => 1, 'pm' => 1 ], 'brokerage_to_nowMoney' => [ 'title' => '佣金提现到余额', 'type' => 'extract', 'mark' => '佣金提现到余额{%num%}元', 'status' => 1, 'pm' => 1 ], 'lottery_use_money' => [ 'title' => '参与抽奖使用余额', 'type' => 'lottery_use', 'mark' => '参与抽奖使用{%num%}余额', 'status' => 1, 'pm' => 0 ], 'lottery_give_money' => [ 'title' => '抽奖中奖赠送余额', 'type' => 'lottery_add', 'mark' => '抽奖中奖赠送{%num%}余额', 'status' => 1, 'pm' => 1 ], 'register_system_add' => [ 'title' => '新用户注册赠送余额', 'type' => 'register_system_add', 'mark' => '新用户注册赠送{%num%}余额', 'status' => 1, 'pm' => 1 ], ]; /** * UserMoneyServices constructor. * @param UserMoneyDao $dao */ public function __construct(UserMoneyDao $dao) { $this->dao = $dao; } /** * 余额流水类型 → 中文标题(按 incomeData 的 type 字段匹配,兼容按配置键查找) * @param string $type * @return string */ public function getTypeTitle(string $type): string { $type = trim($type); if ($type === '') { return ''; } foreach ($this->incomeData as $key => $item) { if (($item['type'] ?? '') === $type || (string)$key === $type) { return (string)($item['title'] ?? $type); } } return $type; } /** * 全部余额类型中文映射(type => title) * @return array */ public function getTypeTitleMap(): array { $map = []; foreach ($this->incomeData as $key => $item) { $title = (string)($item['title'] ?? ''); if ($title === '') { continue; } $map[(string)$key] = $title; if (!empty($item['type'])) { $map[(string)$item['type']] = $title; } } return $map; } /** * 写入用户记录 * @param string $type 写入类型 * @param int $uid * @param int|string|array $number * @param int|string $balance * @param $linkId * @param string $mark * @return bool|mixed */ public function moneyIncome(string $type, int $uid, $number, $linkId, string $mark = '') { $data = $this->incomeData[$type] ?? null; if (!$data) { return true; } /** @var UserServices $userServices */ $userServices = app()->make(UserServices::class); $userInfo = $userServices->get($uid, ['now_money']); if (!$userInfo) { throw new ApiException('用户不存在'); } // 当前余额 $currentMoney = $userInfo['now_money']; if (is_array($number)) { $key = array_keys($number); $key = array_map(function ($item) { return '{%' . $item . '%}'; }, $key); $value = array_values($number); $data['number'] = $number['number'] ?? 0; $data['mark'] = $mark === '' ? str_replace($key, $value, $data['mark']) : $mark; } else { $data['number'] = $number; $data['mark'] = $mark === '' ? str_replace(['{%num%}'], $number, $data['mark']) : $mark; } $data['uid'] = $uid; $data['link_id'] = $linkId; $data['add_time'] = time(); try { // 所有余额变动都采用事务处理,保证数据一致性 return $this->transaction(function () use ($uid, $data, $number, $currentMoney, $userServices) { $lockedUser = app()->make(UserDao::class)->getDb()->where('uid', $uid)->lock(true)->field('uid,now_money')->find(); if (!$lockedUser) throw new ApiException('用户不存在'); $currentMoney = $lockedUser['now_money']; // 更新用户余额(使用乐观锁防止并发超扣) if ($data['pm'] == 0) { if (!$userServices->decrMoney($uid, $data['number'])) { throw new ApiException('余额不足'); } else { // 计算变动后的余额 $balance = bcsub((string)$currentMoney, (string)$data['number'], 2); } } else { if (!$userServices->bcInc($uid, 'now_money', (string)$data['number'])) throw new \RuntimeException('余额更新失败'); // 计算变动后的余额 $balance = bcadd((string)$currentMoney, (string)$data['number'], 2); } $data['balance'] = $balance; // 增加余额记录 if (!$this->dao->save($data)) throw new \RuntimeException('余额流水写入失败'); // 增加余额变动事件 event('UserBalanceChange', [$uid, $data['type'], $number, $balance, $data['mark']]); return true; }); } catch (\Exception $e) { throw new ApiException($e->getMessage().$e->getLine()); } } /** * 写入用户记录 * @param string $type 写入类型 * @param int $uid * @param int|string|array $number * @param int|string $balance * @param $linkId * @param string $mark * @return bool|mixed */ public function income(string $type, int $uid, $number, $balance, $linkId, string $mark = '') { $data = $this->incomeData[$type] ?? null; if (!$data) { return true; } $data['uid'] = $uid; $data['balance'] = $balance ?? 0; $data['link_id'] = $linkId; if (is_array($number)) { $key = array_keys($number); $key = array_map(function ($item) { return '{%' . $item . '%}'; }, $key); $value = array_values($number); $data['number'] = $number['number'] ?? 0; $data['mark'] = $mark === '' ? str_replace($key, $value, $data['mark']) : $mark; } else { $data['number'] = $number; $data['mark'] = $mark === '' ? str_replace(['{%num%}'], $number, $data['mark']) : $mark; } $data['add_time'] = time(); try { // 所有余额变动都采用事务处理,保证数据一致性 return $this->transaction(function () use ($uid, $data, $number, $balance) { // 增加余额记录 $this->dao->save($data); // 更新用户余额(使用乐观锁防止并发超扣) /** @var UserServices $userServices */ $userServices = app()->make(UserServices::class); // 扣款时使用条件更新(乐观锁),确保余额充足 if ($data['pm'] == 0) { if (!$userServices->decrMoney($uid, $number)) { throw new ApiException('余额不足'); } } else { $userServices->update($uid, ['now_money' => number_format((float)$balance, 2, '.', '')]); } // 增加余额变动事件 event('UserBalanceChange', [$uid,$data['type'], $number, $balance, $data['mark']]); return true; }); } catch (\Exception $e) { throw new ApiException($e->getMessage().$e->getLine()); } } /** * 余额记录 * @param $where * @return array */ public function balanceList($where) { $status = []; foreach ($this->incomeData as $value) { $status[$value['type']] = $value['title']; } [$page, $limit] = $this->getPageValue(); $list = $this->dao->getList($where, $page, $limit); //关联用户 /** @var UserServices $userServices */ $userServices = app()->make(UserServices::class); $uids = array_column($list, 'uid'); $nicknameArr = $userServices->getColumn([['uid', 'in', $uids]], 'nickname', 'uid'); //关联订单 /** @var OrderCenterDao $orderServices */ $orderServices = app()->make(OrderCenterDao::class); /** @var MemberOrderServices $memberOrderServices */ $memberOrderServices = app()->make(MemberOrderServices::class); foreach ($list as &$item) { $item['nickname'] = $nicknameArr[$item['uid']]; if ($item['type'] == 'pay_product' || $item['type'] == 'pay_product_refund') { $item['relation'] = $orderServices->value(['id' => $item['link_id']], 'order_no'); } elseif ($item['type'] == 'recharge' || $item['type'] == 'recharge_refund') { /** @var UserRechargeServices $userRechargeServices */ $userRechargeServices = app()->make(UserRechargeServices::class); $item['relation'] = $userRechargeServices->value(['id' => (int)$item['link_id']], 'order_id'); } elseif ($item['type'] == 'pay_member') { $item['relation'] = $memberOrderServices->value(['id' => $item['link_id']], 'order_id'); } else { $item['relation'] = $status[$item['type']]; } $item['add_time'] = date('Y-m-d H:i:s', $item['add_time']); $item['type_name'] = $status[$item['type']]; } $count = $this->dao->count($where); return compact('list', 'count', 'status'); } /** * 余额记录备注 * @param $data * @return bool */ public function recordRemark($id, $mark) { if ($this->dao->update($id, ['mark' => $mark])) { return true; } else { throw new AdminException('备注失败'); } } /** * 余额统计基础 * @return array * @throws \ReflectionException */ public function getBasic() { /** @var UserServices $userServices */ $userServices = app()->make(UserServices::class); $data['now_balance'] = $userServices->sum(['status' => 1], 'now_money', true); $data['add_balance'] = $this->dao->sum([ ['pm', '=', 1], ['type', 'in', ['system_add', 'recharge', 'extract', 'lottery_add', 'register_system_add']] ], 'number', false); $data['sub_balance'] = bcsub($data['add_balance'], $data['now_balance'], 2); return $data; } /** * 余额趋势 * @param $where * @return array */ public function getTrend($where) { $time = explode('-', $where['time']); if (count($time) != 2) throw new AdminException('请选择时间'); $dayCount = (strtotime($time[1]) - strtotime($time[0])) / 86400 + 1; $data = []; if ($dayCount == 1) { $data = $this->trend($time, 0); } elseif ($dayCount > 1 && $dayCount <= 31) { $data = $this->trend($time, 1); } elseif ($dayCount > 31 && $dayCount <= 92) { $data = $this->trend($time, 3); } elseif ($dayCount > 92) { $data = $this->trend($time, 30); } return $data; } /** * 余额趋势 * @param $time * @param $num * @param false $excel * @return array */ public function trend($time, $num, $excel = false) { if ($num == 0) { $xAxis = ['00', '01', '02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23']; $timeType = '%H'; } elseif ($num != 0) { $dt_start = strtotime($time[0]); $dt_end = strtotime($time[1]); while ($dt_start <= $dt_end) { if ($num == 30) { $xAxis[] = date('Y-m', $dt_start); $dt_start = strtotime("+1 month", $dt_start); $timeType = '%Y-%m'; } else { $xAxis[] = date('m-d', $dt_start); $dt_start = strtotime("+$num day", $dt_start); $timeType = '%m-%d'; } } } $time[1] = date("Y-m-d", strtotime("+1 day", strtotime($time[1]))); $point_add = array_column($this->dao->getBalanceTrend($time, $timeType, 'add_time', 'sum(number)', 'add'), 'num', 'days'); $point_sub = array_column($this->dao->getBalanceTrend($time, $timeType, 'add_time', 'sum(number)', 'sub'), 'num', 'days'); $data = $series = []; foreach ($xAxis as $item) { $data['余额积累'][] = isset($point_add[$item]) ? floatval($point_add[$item]) : 0; $data['余额消耗'][] = isset($point_sub[$item]) ? floatval($point_sub[$item]) : 0; } foreach ($data as $key => $item) { $series[] = [ 'name' => $key, 'data' => $item, 'type' => 'line', ]; } return compact('xAxis', 'series'); } /** * 余额来源 * @param $where * @return array */ public function getChannel($where) { $bing_xdata = ['系统增加', '用户充值', '佣金提现', '抽奖赠送', '商品退款']; $color = ['#64a1f4', '#3edeb5', '#70869f', '#ffc653', '#fc7d6a']; $data = ['system_add', 'recharge', 'extract', 'lottery_add', 'pay_product_refund']; $bing_data = []; foreach ($data as $key => $item) { $bing_data[] = [ 'name' => $bing_xdata[$key], 'value' => $this->dao->sum(['pm' => 1, 'type' => $item, 'time' => $where['time']], 'number', true), 'itemStyle' => ['color' => $color[$key]] ]; } $list = []; $count = array_sum(array_column($bing_data, 'value')); foreach ($bing_data as $key => $item) { $list[] = [ 'name' => $item['name'], 'value' => $item['value'], 'percent' => $count != 0 ? bcmul((string)bcdiv((string)$item['value'], (string)$count, 4), '100', 2) : 0, ]; } array_multisort(array_column($list, 'value'), SORT_DESC, $list); return compact('bing_xdata', 'bing_data', 'list'); } /** * 余额类型 * @param $where * @return array */ public function getType($where) { $bing_xdata = ['系统减少', '充值退款', '购买商品', '购买会员']; $color = ['#64a1f4', '#3edeb5', '#70869f', '#ffc653']; $data = ['system_sub', 'recharge_refund', 'pay_product', 'pay_member']; $bing_data = []; foreach ($data as $key => $item) { $bing_data[] = [ 'name' => $bing_xdata[$key], 'value' => $this->dao->sum(['pm' => 0, 'type' => $item, 'time' => $where['time']], 'number', true), 'itemStyle' => ['color' => $color[$key]] ]; } $list = []; $count = array_sum(array_column($bing_data, 'value')); foreach ($bing_data as $key => $item) { $list[] = [ 'name' => $item['name'], 'value' => $item['value'], 'percent' => $count != 0 ? bcmul((string)bcdiv((string)$item['value'], (string)$count, 4), '100', 2) : 0, ]; } array_multisort(array_column($list, 'value'), SORT_DESC, $list); return compact('bing_xdata', 'bing_data', 'list'); } public function getMoneyList($uid, $type) { $where = []; $where['uid'] = $uid; [$page, $limit] = $this->getPageValue(); if ($type == 1) { $where['pm'] = 0; } elseif ($type == 2) { $where['pm'] = 1; $where['not_type'] = ['pay_product_refund']; } $list = $this->dao->getList($where, $page, $limit); $count = $this->dao->count($where); $times = []; if ($list) { foreach ($list as &$item) { $item['time'] = $item['time_key'] = $item['add_time'] ? date('Y-m', (int)$item['add_time']) : ''; $item['add_time'] = $item['add_time'] ? date('Y-m-d H:i', (int)$item['add_time']) : ''; } $times = array_merge(array_unique(array_column($list, 'time_key'))); } return ['list' => $list, 'time' => $times, 'count' => $count]; } /** * 根据查询用户充值金额 * @param array $where * @param string $rechargeSumField * @param string $selectType * @param string $group * @return float|mixed */ public function getRechargeMoneyByWhere(array $where, string $rechargeSumField, string $selectType, string $group = "") { switch ($selectType) { case "sum" : return $this->dao->getWhereSumField($where, $rechargeSumField); case "group" : return $this->dao->getGroupField($where, $rechargeSumField, $group); } } }