diff --git a/application/admin/controller/fc/Userext.php b/application/admin/controller/fc/Userext.php index 46cbed0..b802321 100644 --- a/application/admin/controller/fc/Userext.php +++ b/application/admin/controller/fc/Userext.php @@ -3,6 +3,7 @@ namespace app\admin\controller\fc; use app\common\controller\Backend; +use think\Db; /** * 同城-用户扩展 @@ -25,13 +26,120 @@ class Userext extends Backend } + /** + * 会员列表(关联 fa_user 展示昵称 / 手机号 / 账户余额) + * + * 说明:余额money 存放在核心表 fa_user,本表只有业务扩展字段, + * 因此列表需要 JOIN fa_user 才能看出「谁有多少余额」。 + * 注意 TP5.0 的 count() 会清空查询条件,必须分别构建查询。 + */ + public function index() + { + if ($this->request->isAjax()) { + list($where, $sort, $order, $offset, $limit) = $this->buildparams(); + $total = $this->model->alias('e') + ->join('user u', 'u.id = e.user_id', 'LEFT') + ->where($where) + ->count(); + + $list = $this->model->alias('e') + ->join('user u', 'u.id = e.user_id', 'LEFT') + ->where($where) + ->field('e.*, u.nickname, u.mobile, u.money') + ->order($sort, $order) + ->limit($offset, $limit) + ->select(); + + return json(['total' => $total, 'rows' => $list]); + } + return $this->view->fetch(); + } + + + + /** + * 余额充值(给选中用户增加账户余额) + * + * POST /s.php/fc/userext/recharge + * 入参:{ ids: '1001,1002' , amount: 100, remark?: '备注' } + * + * 说明: + * 余额存放在核心表 fa_user.money,本表(fc_user_ext)仅作业务扩展, + * 因此这里按 user_id 关联 fa_user 进行加减。 + * 为可追溯,每次充值同时写入 fa_fc_finance_flow(资金流水)与 + * fa_fc_message(站内通知),后台操作留痕由 Backend 自动记录。 + */ + public function recharge() + { + $ids = (string)$this->request->post('ids', ''); + $userIds = array_filter(array_map('intval', explode(',', $ids))); + $userIds = array_values(array_unique($userIds)); + if (!$userIds) { + $this->error('请选择要充值的用户'); + } + + $amount = round((float)$this->request->post('amount', 0), 2); + if ($amount <= 0) { + $this->error('充值金额需大于 0'); + } + if ($amount > 100000) { + $this->error('单次充值金额过大'); + } + $remark = trim((string)$this->request->post('remark', '')); + $now = time(); + $done = 0; + + foreach ($userIds as $uid) { + $user = Db::name('user')->where('id', $uid)->find(); + if (!$user) { + continue; + } + + $balance = round((float)$user['money'] + $amount, 2); + Db::name('user')->where('id', $uid)->update([ + 'money' => $balance, + 'updatetime' => $now, + ]); + + // 资金流水(平台口径:余额充值属于收入) + Db::name('fc_finance_flow')->insert([ + 'flow_no' => 'R' . date('YmdHis') . mt_rand(100, 999) . $uid, + 'type' => 'income', + 'scene' => '余额充值', + 'amount' => $amount, + 'balance' => $balance, + 'related_id' => $uid, + 'remark' => $remark !== '' ? $remark : ('后台充值给用户 #' . $uid), + 'createtime' => $now, + ]); + + // 站内通知 + Db::name('fc_message')->insert([ + 'user_id' => $uid, + 'type' => 'system', + 'title' => '余额充值到账', + 'content' => '您的账户已充值 ¥' . number_format($amount, 2, '.', '') + . ',当前余额 ¥' . number_format($balance, 2, '.', '') + . '。' . ($remark !== '' ? '备注:' . $remark : ''), + 'isread' => 0, + 'readtime' => null, + 'createtime' => $now, + ]); + + $done++; + } + + if (!$done) { + $this->error('未找到对应用户'); + } + + $this->success('充值成功:' . $done . ' 个用户,每人 ¥' . number_format($amount, 2, '.', '')); + } /** * 默认生成的控制器所继承的父类中有index/add/edit/del/multi五个基础方法、destroy/restore/recyclebin三个回收站方法 * 因此在当前控制器中可不用编写增删改查的代码,除非需要自己控制这部分逻辑 * 需要将application/admin/library/traits/Backend.php中对应的方法复制到当前控制器,然后进行修改 */ - - } diff --git a/application/admin/view/fc/userext/index.html b/application/admin/view/fc/userext/index.html index 5963d6e..d0c894b 100644 --- a/application/admin/view/fc/userext/index.html +++ b/application/admin/view/fc/userext/index.html @@ -10,6 +10,7 @@ {:__('Add')} {:__('Edit')} {:__('Delete')} + 余额充值 diff --git a/application/api/controller/Member.php b/application/api/controller/Member.php index fbdf32a..b2f5c0a 100644 --- a/application/api/controller/Member.php +++ b/application/api/controller/Member.php @@ -99,6 +99,12 @@ class Member extends V1 $amount = round((float)$plan['price'], 2); $now = time(); + // 余额校验必须在落订单之前:否则余额不足时已插入一条 status=paid 的假订单 + $balance = (float)$user['money']; + if ($payType === 'balance' && $balance < $amount) { + $this->fail('余额不足'); + } + $orderNo = $this->orderNo('MO'); $orderId = Db::name('fc_order')->insertGetId([ 'order_no' => $orderNo, @@ -115,10 +121,6 @@ class Member extends V1 ]); if ($payType === 'balance') { - $balance = (float)$user['money']; - if ($balance < $amount) { - $this->fail('余额不足'); - } $balance = round($balance - $amount, 2); Db::name('user')->where('id', $user['id'])->update([ 'money' => $balance, diff --git a/application/api/controller/Post.php b/application/api/controller/Post.php index 7f87d68..c614cc1 100644 --- a/application/api/controller/Post.php +++ b/application/api/controller/Post.php @@ -57,11 +57,18 @@ class Post extends V1 $payType = (string)$this->input('payType', ''); $amount = 0; if ($paid) { + if (!in_array($payType, ['wxpay', 'balance'], true)) { + $this->fail('支付方式不支持'); + } $amount = $this->input('amount', null); if ($amount === null || $amount === '') { $amount = $this->cfg('fee.singlePost', 5); } $amount = round((float)$amount, 2); + // 余额校验须在落库之前:否则余额不足时会残留一条 is_paid=1 的无效发布 + if ($payType === 'balance' && (float)$user['money'] < $amount) { + $this->fail('余额不足'); + } } $now = time(); @@ -94,6 +101,9 @@ class Post extends V1 $order = null; if ($paid) { + // 余额支付为「即时支付」:下单即扣款、订单直接置为已支付; + // 微信支付需要前端调起收银台,保持 pending 由支付回调置为 paid。 + $paidNow = $payType === 'balance'; $orderNo = $this->orderNo('PO'); Db::name('fc_order')->insert([ 'order_no' => $orderNo, @@ -102,12 +112,28 @@ class Post extends V1 'title' => $title, 'amount' => $amount, 'pay_type' => $payType, - 'status' => 'pending', + 'status' => $paidNow ? 'paid' : 'pending', + 'paytime' => $paidNow ? $now : null, 'related_id' => $postId, 'createtime' => $now, 'updatetime' => $now, ]); - $order = ['orderNo' => $orderNo, 'amount' => $amount, 'payType' => $payType]; + + if ($paidNow) { + $balance = round((float)$user['money'] - $amount, 2); + Db::name('user')->where('id', $user['id'])->update([ + 'money' => $balance, + 'updatetime' => $now, + ]); + $this->flow('income', '发布付费', $amount, $balance, $postId); + } + + $order = [ + 'orderNo' => $orderNo, + 'amount' => $amount, + 'payType' => $payType, + 'status' => $paidNow ? 'paid' : 'pending', + ]; } $this->ok([ diff --git a/public/assets/js/backend/fc/userext.js b/public/assets/js/backend/fc/userext.js index 4c0141f..6ecc5fc 100644 --- a/public/assets/js/backend/fc/userext.js +++ b/public/assets/js/backend/fc/userext.js @@ -28,19 +28,38 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin [ {checkbox: true}, {field: 'id', title: __('Id')}, - {field: 'user_id', title: __('User_id')}, - {field: 'openid', title: __('Openid'), operate: 'LIKE'}, - {field: 'user_type', title: __('User_type')}, - {field: 'realname_status', title: __('Realname_status')}, - {field: 'is_member', title: __('Is_member')}, - {field: 'member_level', title: __('Member_level'), operate: 'LIKE'}, - {field: 'member_expiretime', title: __('Member_expiretime'), operate:'RANGE', addclass:'datetimerange', autocomplete:false, formatter: Table.api.formatter.datetime}, - {field: 'invite_code', title: __('Invite_code'), operate: 'LIKE'}, - {field: 'inviter_id', title: __('Inviter_id')}, - {field: 'free_view_date', title: __('Free_view_date'), operate: 'LIKE'}, - {field: 'free_view_used', title: __('Free_view_used')}, - {field: 'createtime', title: __('Createtime'), operate:'RANGE', addclass:'datetimerange', autocomplete:false, formatter: Table.api.formatter.datetime}, - {field: 'updatetime', title: __('Updatetime'), operate:'RANGE', addclass:'datetimerange', autocomplete:false, formatter: Table.api.formatter.datetime}, + {field: 'user_id', title: '用户ID'}, + {field: 'nickname', title: '昵称', operate: 'LIKE'}, + {field: 'mobile', title: '手机号', operate: 'LIKE'}, + { + field: 'money', title: '账户余额', operate: false, + formatter: function (value) { + var n = parseFloat(value || 0); + return n > 0 ? '¥' + n.toFixed(2) : '¥0.00'; + } + }, + { + field: 'user_type', title: '身份', searchList: {'1': '个人', '2': '商家'}, + formatter: function (value) { + return value == 2 ? '商家' : '个人'; + } + }, + { + field: 'is_member', title: '会员', searchList: {'1': '是', '0': '否'}, + formatter: function (value) { + return value == 1 ? '是' : '否'; + } + }, + {field: 'member_level', title: '会员等级', operate: 'LIKE'}, + {field: 'member_expiretime', title: '会员到期', operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, formatter: Table.api.formatter.datetime}, + {field: 'invite_code', title: '邀请码', operate: 'LIKE'}, + {field: 'openid', title: __('Openid'), operate: 'LIKE', visible: false}, + {field: 'realname_status', title: __('Realname_status'), visible: false}, + {field: 'inviter_id', title: __('Inviter_id'), visible: false}, + {field: 'free_view_date', title: __('Free_view_date'), visible: false}, + {field: 'free_view_used', title: __('Free_view_used'), visible: false}, + {field: 'createtime', title: __('Createtime'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, formatter: Table.api.formatter.datetime}, + {field: 'updatetime', title: __('Updatetime'), operate: 'RANGE', addclass: 'datetimerange', autocomplete: false, formatter: Table.api.formatter.datetime, visible: false}, {field: 'operate', title: __('Operate'), table: table, events: Table.api.events.operate, formatter: Table.api.formatter.operate} ] ] @@ -48,6 +67,30 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form'], function ($, undefin // 为表格绑定事件 Table.api.bindevent(table); + + // 余额充值:需先勾选用户,再输入金额 + $(document).on('click', '.btn-recharge', function () { + var ids = Table.api.selectedids(table); + if (!ids.length) { + Layer.msg('请先勾选要充值的用户'); + return; + } + Layer.prompt({title: '请输入充值金额(元)', formType: 0}, function (value, index) { + var amount = parseFloat(value); + if (!(amount > 0)) { + Layer.msg('充值金额需大于 0'); + return; + } + Fast.api.ajax({ + url: 'fc/userext/recharge', + data: {ids: ids.join(','), amount: amount} + }, function (data) { + table.bootstrapTable('refresh'); + return false; + }); + Layer.close(index); + }); + }); }, add: function () { Controller.api.bindevent();