diff --git a/addons/epay/library/v3new/Yansongda/Pay/Shortcut/Wechat/MchTransferShortcut.php b/addons/epay/library/v3new/Yansongda/Pay/Shortcut/Wechat/MchTransferShortcut.php index aa80f17..907b879 100644 --- a/addons/epay/library/v3new/Yansongda/Pay/Shortcut/Wechat/MchTransferShortcut.php +++ b/addons/epay/library/v3new/Yansongda/Pay/Shortcut/Wechat/MchTransferShortcut.php @@ -70,6 +70,7 @@ class MchTransferShortcut implements ShortcutInterface return [ StartPlugin::class, QueryPlugin::class, + AddPayloadBodyPlugin::class, AddPayloadSignaturePlugin::class, AddRadarPlugin::class, VerifySignaturePlugin::class, @@ -83,6 +84,7 @@ class MchTransferShortcut implements ShortcutInterface return [ StartPlugin::class, \Yansongda\Pay\Plugin\Wechat\V3\Marketing\MchTransfer\CancelPlugin::class, + AddPayloadBodyPlugin::class, AddPayloadSignaturePlugin::class, AddRadarPlugin::class, VerifySignaturePlugin::class, diff --git a/application/admin/controller/fc/Userext.php b/application/admin/controller/fc/Userext.php index fe0086c..0111bfb 100644 --- a/application/admin/controller/fc/Userext.php +++ b/application/admin/controller/fc/Userext.php @@ -4,11 +4,13 @@ namespace app\admin\controller\fc; use app\common\controller\Backend; use think\Db; +use think\exception\PDOException; +use think\exception\ValidateException; /** - * 同城-用户扩展 + * 同城-用户扩展与用户管理控制器 * - * @icon fa fa-circle-o + * @icon fa fa-user-circle */ class Userext extends Backend { @@ -19,19 +21,26 @@ class Userext extends Backend */ protected $model = null; + /** + * 无需鉴权的公开方法(详情弹窗开放给后台已登录管理角色查看) + */ + protected $noNeedRight = ['detail']; + + /** + * 控制器初始化 + * + * @return void + */ public function _initialize() { parent::_initialize(); $this->model = new \app\admin\model\FcUserExt; - } /** - * 会员列表(关联 fa_user 展示昵称 / 手机号 / 账户余额) + * 会员列表(关联 fa_user 展示昵称 / 手机号 / 账户余额 / 真实姓名) * - * 说明:余额money 存放在核心表 fa_user,本表只有业务扩展字段, - * 因此列表需要 JOIN fa_user 才能看出「谁有多少余额」。 - * 注意 TP5.0 的 count() 会清空查询条件,必须分别构建查询。 + * @return string|\think\response\Json */ public function index() { @@ -56,21 +65,238 @@ class Userext extends Backend return $this->view->fetch(); } + /** + * 用户详细信息综合看板(包含基本资料、实名信息、微信提现账户、分销佣金、会员特权及近期提现记录) + * + * @param int|null $ids 用户扩展记录ID + * @return string + */ + public function detail($ids = null) + { + $id = $ids ? $ids : $this->request->param('ids'); + $row = $this->model->get($id); + if (!$row) { + $this->error(__('No Results were found')); + } + $userId = (int)$row['user_id']; + + // 1. 主表 fa_user 用户基本信息 + $user = Db::name('user')->where('id', $userId)->find(); + if (!$user) { + $user = [ + 'id' => $userId, + 'nickname' => '未注册用户', + 'mobile' => '-', + 'avatar' => '', + 'money' => '0.00', + 'score' => 0, + 'createtime' => null, + 'logintime' => null, + 'loginip' => '', + ]; + } + + // 2. 上级邀请人信息 + $inviterUser = null; + if (!empty($row['inviter_id'])) { + $inviterUser = Db::name('user') + ->where('id', (int)$row['inviter_id']) + ->field('id,nickname,mobile,avatar') + ->find(); + } + + // 3. 分销与佣金统计 + $commissionTotal = (float)Db::name('fc_commission') + ->where('user_id', $userId) + ->where('status', '<>', 2) + ->sum('amount'); + + $commissionSettled = (float)Db::name('fc_commission') + ->where('user_id', $userId) + ->where('status', 1) + ->sum('amount'); + + $commissionPending = (float)Db::name('fc_commission') + ->where('user_id', $userId) + ->where('status', 0) + ->sum('amount'); + + // 累计已成功提现金额 + $withdrawPaidTotal = (float)Db::name('fc_withdraw') + ->where('user_id', $userId) + ->where('status', 'paid') + ->sum('amount'); + + // 已占用提现金额(包含审核中、转账中、已打款) + $withdrawAppliedTotal = (float)Db::name('fc_withdraw') + ->where('user_id', $userId) + ->where('status', 'in', ['auditing', 'paying', 'paid']) + ->sum('amount'); + + // 当前剩余可提现金额 + $withdrawable = max(0, $commissionSettled - $withdrawAppliedTotal); + + // 下级团队人数统计(一级、二级) + $level1Count = (int)Db::name('fc_dist_relation') + ->where('parent_id', $userId) + ->where('level', 1) + ->count(); + + $level2Count = (int)Db::name('fc_dist_relation') + ->where('parent_id', $userId) + ->where('level', 2) + ->count(); + + // 4. 业务数据概览(发帖数与消费订单数) + $postCount = (int)Db::name('fc_post')->where('user_id', $userId)->count(); + $orderCount = (int)Db::name('fc_order')->where('user_id', $userId)->where('status', 'paid')->count(); + $orderSpend = (float)Db::name('fc_order')->where('user_id', $userId)->where('status', 'paid')->sum('amount'); + + // 5. 最近 5 笔提现记录 + $recentWithdraws = Db::name('fc_withdraw') + ->where('user_id', $userId) + ->order('id', 'desc') + ->limit(5) + ->select(); + + // 6. 最近 5 笔佣金明细 + $recentCommissions = Db::name('fc_commission')->alias('c') + ->join('user fu', 'fu.id = c.from_user_id', 'LEFT') + ->where('c.user_id', $userId) + ->field('c.*, fu.nickname as from_nickname') + ->order('c.id', 'desc') + ->limit(5) + ->select(); + + $this->view->assign([ + 'row' => $row, + 'userInfo' => $user, + 'inviterUser' => $inviterUser, + 'commissionTotal' => round($commissionTotal, 2), + 'commissionSettled' => round($commissionSettled, 2), + 'commissionPending' => round($commissionPending, 2), + 'withdrawPaidTotal' => round($withdrawPaidTotal, 2), + 'withdrawable' => round($withdrawable, 2), + 'level1Count' => $level1Count, + 'level2Count' => $level2Count, + 'postCount' => $postCount, + 'orderCount' => $orderCount, + 'orderSpend' => round($orderSpend, 2), + 'recentWithdraws' => $recentWithdraws, + 'recentCommissions' => $recentCommissions, + ]); + + return $this->view->fetch(); + } + + /** + * 编辑用户扩展资料(包含实名姓名、会员等级、发帖额度等) + * + * @param int|null $ids 用户扩展记录ID + * @return string + */ + public function edit($ids = null) + { + $id = $ids ? $ids : $this->request->param('ids'); + $row = $this->model->get($id); + if (!$row) { + $this->error(__('No Results were found')); + } + + if ($this->request->isPost()) { + $params = $this->request->post('row/a'); + if ($params) { + $params = $this->preExcludeFields($params); + + // 1. 真实姓名与实名状态清洗 + if (isset($params['realname'])) { + $params['realname'] = trim((string)$params['realname']); + } + if (isset($params['realname_status'])) { + $params['realname_status'] = (int)$params['realname_status'] === 1 ? 1 : 0; + } + + // 2. 用户身份(1: 个人用户, 2: 商家) + if (isset($params['user_type'])) { + $params['user_type'] = (int)$params['user_type'] === 2 ? 2 : 1; + } + + // 3. 会员状态与到期时间处理 + if (isset($params['is_member'])) { + $params['is_member'] = (int)$params['is_member'] === 1 ? 1 : 0; + } + if (isset($params['member_expiretime'])) { + $expire = trim((string)$params['member_expiretime']); + $params['member_expiretime'] = $expire !== '' ? (is_numeric($expire) ? (int)$expire : strtotime($expire)) : null; + } + + // 4. 发帖配额及到期时间处理 + if (isset($params['post_quota_left'])) { + $params['post_quota_left'] = max(0, (int)$params['post_quota_left']); + } + if (isset($params['post_quota_expire'])) { + $expire = trim((string)$params['post_quota_expire']); + $params['post_quota_expire'] = $expire !== '' ? (is_numeric($expire) ? (int)$expire : strtotime($expire)) : null; + } + + // 5. 免费电话查看配额处理 + if (isset($params['free_view_quota'])) { + $params['free_view_quota'] = max(0, (int)$params['free_view_quota']); + } + if (isset($params['free_view_used'])) { + $params['free_view_used'] = max(0, (int)$params['free_view_used']); + } + + // 6. 邀请人处理(避免自身成为自身邀请人) + if (isset($params['inviter_id'])) { + $inviterId = (int)$params['inviter_id']; + if ($inviterId > 0 && $inviterId === (int)$row['user_id']) { + $this->error('上级邀请人不能设置为当前用户本人'); + } + $params['inviter_id'] = $inviterId; + } + + Db::startTrans(); + try { + $row->allowField(true)->save($params); + Db::commit(); + } catch (ValidateException $e) { + Db::rollback(); + $this->error($e->getMessage()); + } catch (PDOException $e) { + Db::rollback(); + $this->error($e->getMessage()); + } catch (\Exception $e) { + Db::rollback(); + $this->error($e->getMessage()); + } + $this->success('更新成功'); + } + $this->error(__('Parameter %s can not be empty', '')); + } + + // 关联主表用户信息 + $userInfo = Db::name('user')->where('id', (int)$row['user_id'])->find(); + $inviterInfo = null; + if (!empty($row['inviter_id'])) { + $inviterInfo = Db::name('user')->where('id', (int)$row['inviter_id'])->field('id,nickname,mobile')->find(); + } + + $this->view->assign([ + 'row' => $row, + 'userInfo' => $userInfo ?: [], + 'inviterInfo' => $inviterInfo, + ]); + + 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 进行加减。 - * 【注意】ids 必须是 fa_user.id(即本表的 user_id 字段), - * 不能传 fc_user_ext.id,否则会充错人(或报「未找到对应用户」)。 - * 为可追溯,每次充值同时写入 fa_fc_finance_flow(资金流水)与 - * fa_fc_message(站内通知),后台操作留痕由 Backend 自动记录。 */ public function recharge() { @@ -166,10 +392,4 @@ class Userext extends Backend } $this->success($msg); } - - /** - * 默认生成的控制器所继承的父类中有index/add/edit/del/multi五个基础方法、destroy/restore/recyclebin三个回收站方法 - * 因此在当前控制器中可不用编写增删改查的代码,除非需要自己控制这部分逻辑 - * 需要将application/admin/library/traits/Backend.php中对应的方法复制到当前控制器,然后进行修改 - */ } diff --git a/application/admin/controller/fc/Withdraw.php b/application/admin/controller/fc/Withdraw.php index 48742b5..cd27ad9 100644 --- a/application/admin/controller/fc/Withdraw.php +++ b/application/admin/controller/fc/Withdraw.php @@ -203,9 +203,9 @@ class Withdraw extends Backend $this->success('驳回成功(' . $done . ' 条)'); } - // 通过 → 逐笔发起微信商家转账(外部调用不能放在事务里) + // 通过 → 逐笔发起微信商家转账(支持 auditing 与 paying 重试,外部调用不能放在事务里) foreach ($withdraws as $withdraw) { - if ((string)$withdraw['status'] !== 'auditing') { + if (!in_array((string)$withdraw['status'], ['auditing', 'paying'], true)) { continue; } try { diff --git a/application/admin/view/fc/userext/detail.html b/application/admin/view/fc/userext/detail.html new file mode 100644 index 0000000..547d14e --- /dev/null +++ b/application/admin/view/fc/userext/detail.html @@ -0,0 +1,481 @@ + + +
+ + +
+
+
+
+ +
+
+ {$userInfo.nickname|default='未设置昵称'} + UID: {$row.user_id} + {if condition="$row.user_type == 2"} + 认证商家 + {else /} + 个人用户 + {/if} + {if condition="!empty($row.is_member)"} + 会员 ({$row.member_level|default='VIP'}) + {/if} +
+
+ 手机号:{$userInfo.mobile|default='未绑定'} + 注册时间:{$userInfo.createtime ? datetime($userInfo.createtime) : '-'} + 最后登录:{$userInfo.logintime ? datetime($userInfo.logintime) : '-'} ({$userInfo.loginip|default='-'}) +
+
+
+
+ 编辑资料 +
+
+
+
+ + +
+
+ 账户余额 +
¥{$userInfo.money|default='0.00'}
+
可用于发帖置顶与支付
+
+
+ 可提现佣金 +
¥{$withdrawable}
+
已结算且未被占用的佣金
+
+
+ 累计已提现 +
¥{$withdrawPaidTotal}
+
成功打款至微信零钱总额
+
+
+ 分销佣金总额 +
¥{$commissionTotal}
+
已结算 ¥{$commissionSettled} / 待结 ¥{$commissionPending}
+
+
+ 推广团队下线 +
{$level1Count + $level2Count} 人
+
一级 {$level1Count} 人 | 二级 {$level2Count} 人
+
+
+ + +
+
+

+ 微信实名认证与佣金提现账户 +

+
+ {if condition="!empty($row.realname_status)"} + 已实名认证 + {else /} + 未实名认证 + {/if} +
+
+
+
+
+ + + + + + + + + +
收款人真实姓名 + {if condition="!empty($row.realname)"} + + {$row.realname} + + 微信核验一致 + {else /} + 未填写收款真实姓名 + (暂无法发起微信转账) + {/if} +
实名认证状态 + {if condition="!empty($row.realname_status)"} + 已认证 (提现受微信校验保护) + {else /} + 未认证 + {/if} +
+
+ +
+ + + + + + + + + +
微信收款标识 (OpenID) + {if condition="!empty($row.openid)"} + {$row.openid} + {else /} + 未绑定微信OpenID + {/if} +
微信商家转账状态 + {if condition="!empty($row.openid) && !empty($row.realname)"} + 资料完备,支持零钱直连打款 + {else /} + 缺少{if condition="empty($row.realname)"}真实姓名{else /}OpenID{/if},提现需先完善 + {/if} +
+
+
+ + +
+
+ 佣金提现与实名机制说明: +
+
+
1. 提现时如何实名:当用户在小程序端【分销推广中心 -> 申请提现】时,若此前未录入收款人真实姓名,系统会主动弹出输入框要求用户填写「收款人真实姓名(需与微信实名一致)」。用户提交后,系统自动调用实名接口并保存为本处的「真实姓名」,同时将实名状态标记为「已实名」。后续提现自动沿用,无需用户再次填写。
+
2. 后台录入与核对:管理员可在当前页面右上角的【编辑资料】弹窗中,直接查看、录入或修正用户的真实姓名与实名状态。
+
3. 微信转账强校验:系统通过微信支付「商家转账到零钱」进行佣金结算打款时,微信官方接口会强行比对用户微信支付的真实姓名与后台提交的真实姓名。两相一致方可划款成功;若不一致,转账失败且资金不扣减。
+
+
+
+
+ + +
+ +
+
+
+

会员权益与发布配额

+
+
+ + + + + + + + + + + + + + + + + + + + + +
会员状态 + {if condition="!empty($row.is_member)"} + 会员 + {else /} + 普通用户 + {/if} +
会员等级{$row.member_level|default='-'}
会员到期时间 + {if condition="!empty($row.member_expiretime)"} + {$row.member_expiretime|datetime} + {if condition="$row.member_expiretime < time()"} + 已过期 + {else /} + 有效 + {/if} + {else /} + - + {/if} +
赠送发帖配额 + 剩余 {$row.post_quota_left|default=0} 条 + {if condition="!empty($row.post_quota_expire)"} + (有效期至: {$row.post_quota_expire|datetime}) + {/if} +
查电话免费额度 + 今日已用 {$row.free_view_used|default=0} / 每日额度 {$row.free_view_quota|default=20} 次 +
+
+
+
+ + +
+
+
+

推广邀请与团队上下级

+
+
+ + + + + + + + + + + + + + + + + + + + + +
专属邀请码 + {$row.invite_code|default='-'} +
上级推荐人 + {if condition="!empty($inviterUser)"} +
+ + {$inviterUser.nickname|default='-'} + (UID: {$inviterUser.id}) + {$inviterUser.mobile|default='-'} +
+ {else /} + 无上级邀请人 (根用户) + {/if} +
团队规模 + 一级下线:{$level1Count} 人 + | + 二级下线:{$level2Count} 人 +
累计发布信息{$postCount} 条同城信息
累计平台消费{$orderCount} 笔支付订单 (共 ¥{$orderSpend})
+
+
+
+
+ + +
+ +
+
+
+

近期提现申请 (最近5笔)

+
+
+ + + + + + + + + + + + {volist name="recentWithdraws" id="w"} + + + + + + + + {/volist} + {if condition="empty($recentWithdraws)"} + + + + {/if} + +
单号金额渠道状态申请时间
{$w.withdraw_no}¥{$w.amount} + {if condition="$w.channel == 'wxpay'"} + 微信零钱 + {else /} + {$w.channel} + {/if} + + {switch name="w.status"} + {case value="auditing"}待审核{/case} + {case value="paying"}转账中{/case} + {case value="paid"}已到账{/case} + {case value="rejected"}已驳回{/case} + {default /}{$w.status} + {/switch} + {$w.createtime|datetime}
暂无提现记录
+
+
+
+ + +
+
+
+

近期分销佣金明细 (最近5笔)

+
+
+ + + + + + + + + + + + {volist name="recentCommissions" id="c"} + + + + + + + + {/volist} + {if condition="empty($recentCommissions)"} + + + + {/if} + +
单号来源下线佣金分佣比结算状态
{$c.order_no}{$c.from_nickname|default=('UID:'.$c.from_user_id)}+¥{$c.amount}{$c.rate}% + {if condition="$c.status == 1"} + 已结算 + {elseif condition="$c.status == 0" /} + 待结算 + {else /} + 已失效 + {/if} +
暂无佣金明细
+
+
+
+
+ +
diff --git a/application/admin/view/fc/userext/edit.html b/application/admin/view/fc/userext/edit.html index 35bb01e..76c2258 100644 --- a/application/admin/view/fc/userext/edit.html +++ b/application/admin/view/fc/userext/edit.html @@ -1,75 +1,211 @@
-
- -
- + +
+
+
+
+ +
+
+ {$userInfo.nickname|default='未设置昵称'} + UID: {$row.user_id} +
+
+ {$userInfo.mobile|default='未绑定手机'} + | + 注册于:{if condition="!empty($userInfo.createtime)"}{$userInfo.createtime|datetime}{else /}-{/if} +
+
+
+
+ 账户钱包余额 +
¥{$userInfo.money|default='0.00'}
+
+
-
- -
- + + +
+
+

+ 身份与微信实名(佣金提现核验) +

+
+
+ +
+ +
+
+ + +
+ 认证商家拥有专属商户主页及特定发布特权。 +
+
+ +
+ +
+ +
+ 提现说明:分销佣金提现经微信「商家转账到零钱」接口打款时,微信会强校验此姓名与微信支付实名是否完全一致。 +
+
+
+ +
+ +
+
+ + +
+ 用户首次在小程序提现时填写姓名会自动认证;管理员在此修改也会同步生效。 +
+
+ +
+ +
+ + 微信小程序用户标识,商家转账直接转入此微信绑定零钱账户。 +
+
+
-
- -
- + + +
+
+

+ 会员特权与配额配置 +

+
+
+ +
+ +
+
+ + +
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ + 会员套餐赠送或后台特别分配的免费发帖额度。 +
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+
+ 每日配额 + + 次 +
+
+
+
+ 今日已用 + + 次 +
+
+
+
+
+
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- -
-
-
- -
- + + +
+
+

+ 推广邀请与上下级关系 +

+
+
+ +
+ +
+ + 该用户专属的分销邀请码,新用户扫码或绑定此码即可成为其下线。 +
+
+ +
+ +
+ + {if condition="!empty($inviterInfo)"} + + 当前上级:{$inviterInfo.nickname|default='-'} (手机号: {$inviterInfo.mobile|default='-'}) + + {/if} +
+
+
+ diff --git a/application/api/controller/Auth.php b/application/api/controller/Auth.php index 48eff2f..26c64a6 100644 --- a/application/api/controller/Auth.php +++ b/application/api/controller/Auth.php @@ -70,6 +70,37 @@ class Auth extends V1 $ext = Db::name('fc_user_ext')->where('openid', $openid)->find(); $user = $ext ? Db::name('user')->where('id', $ext['user_id'])->find() : null; + // 跨端归一:若按 openid 未查到,但带有手机号,且系统中已存在该手机号用户,则直接归一绑定微信,避免重复建号 + if (!$user && $phone !== '') { + $existUser = Db::name('user')->where('mobile', $phone)->find(); + if ($existUser) { + $user = $existUser; + $existExt = Db::name('fc_user_ext')->where('user_id', $user['id'])->find(); + if ($existExt) { + Db::name('fc_user_ext')->where('user_id', $user['id'])->update([ + 'openid' => $openid, + 'updatetime' => time(), + ]); + } else { + Db::name('fc_user_ext')->insert([ + 'user_id' => $user['id'], + 'openid' => $openid, + 'user_type' => 1, + 'realname_status' => 0, + 'is_member' => 0, + 'member_level' => '', + 'member_expiretime' => null, + 'invite_code' => strtoupper(substr(md5('fc' . $user['id']), 0, 6)), + 'inviter_id' => 0, + 'free_view_date' => date('Y-m-d'), + 'free_view_used' => 0, + 'createtime' => time(), + 'updatetime' => time(), + ]); + } + } + } + if (!$user) { // 首次登录:自动建号(有微信昵称/头像就落库,否则用占位昵称 + 字母头像) $uid = Db::name('user')->insertGetId([ @@ -153,6 +184,10 @@ class Auth extends V1 if (!preg_match('/^1\d{10}$/', $phone)) { $this->fail('手机号格式不正确'); } + $conflict = Db::name('user')->where('mobile', $phone)->where('id', '<>', $user['id'])->find(); + if ($conflict) { + $this->fail('该手机号已被其他账号绑定,请勿重复使用'); + } Db::name('user')->where('id', $user['id'])->update([ 'mobile' => $phone, 'updatetime' => time(), ]); @@ -183,6 +218,11 @@ class Auth extends V1 $this->fail($e->getMessage() ?: '微信手机号获取失败,请改用手动输入'); } + $conflict = Db::name('user')->where('mobile', $phone)->where('id', '<>', $user['id'])->find(); + if ($conflict) { + $this->fail('该手机号已被其他账号绑定,请勿重复使用'); + } + Db::name('user')->where('id', $user['id'])->update([ 'mobile' => $phone, 'updatetime' => time(), ]); @@ -277,6 +317,7 @@ class Auth extends V1 if ($this->request->isPost()) { $nickname = trim((string)$this->input('nickname', '')); $avatar = trim((string)$this->input('avatar', '')); + $realname = trim((string)$this->input('realname', '')); $update = []; if ($nickname !== '') { if (mb_strlen($nickname) > 32) { @@ -287,12 +328,27 @@ class Auth extends V1 if ($avatar !== '') { $update['avatar'] = $avatar; } - if (!$update) { + if ($update) { + $update['updatetime'] = time(); + Db::name('user')->where('id', $user['id'])->update($update); + $user = Db::name('user')->where('id', $user['id'])->find(); + } + + // 支持在个人资料中直接修改真实姓名(用于提现微信核验) + if ($realname !== '') { + if (!preg_match('/^[\x{4e00}-\x{9fa5}·]{2,20}$/u', $realname)) { + $this->fail('请填写 2-20 位中文真实姓名'); + } + Db::name('fc_user_ext')->where('user_id', $user['id'])->update([ + 'realname' => $realname, + 'realname_status' => 1, + 'updatetime' => time(), + ]); + } + + if (!$update && $realname === '') { $this->fail('没有需要更新的资料'); } - $update['updatetime'] = time(); - Db::name('user')->where('id', $user['id'])->update($update); - $user = Db::name('user')->where('id', $user['id'])->find(); } $this->ok($this->userInfo($user)); diff --git a/application/api/controller/Distribution.php b/application/api/controller/Distribution.php index 8325ca8..28eb200 100644 --- a/application/api/controller/Distribution.php +++ b/application/api/controller/Distribution.php @@ -352,6 +352,11 @@ class Distribution extends V1 $rows = Db::name('fc_withdraw')->where('user_id', $user['id']) ->order('createtime desc')->select(); + $epay = (array)get_addon_config('epay'); + $wx = isset($epay['wechat']) && is_array($epay['wechat']) ? $epay['wechat'] : []; + $mchId = isset($wx['mch_id']) ? (string)$wx['mch_id'] : '1118898018'; + $appId = isset($wx['miniapp_id']) ? (string)$wx['miniapp_id'] : 'wxbfcb778f3019cff8'; + $list = []; foreach ($rows as $row) { $list[] = [ @@ -363,6 +368,8 @@ class Distribution extends V1 'packageInfo' => (string)(isset($row['package_info']) ? $row['package_info'] : ''), 'failReason' => (string)(isset($row['fail_reason']) ? $row['fail_reason'] : ''), 'createdAt' => $this->iso($row['createtime']), + 'transferMchId' => $mchId, + 'transferAppId' => $appId, ]; } diff --git a/application/common/library/WxTransfer.php b/application/common/library/WxTransfer.php index fbf4c41..37ac393 100644 --- a/application/common/library/WxTransfer.php +++ b/application/common/library/WxTransfer.php @@ -161,6 +161,7 @@ class WxTransfer * * 流程:auditing → paying(落 out_bill_no)→ 调微信创建转账单 → 立即结果 / 等待回调 * 失败时回滚为 auditing,允许重新审核。 + * 若处于 paying 状态重试,先向微信查询单号状态,避免重复创建转账单。 * * @param int $withdrawId * @return array{state:string,withdrawState:string} @@ -172,17 +173,31 @@ class WxTransfer if (!$withdraw) { throw new \RuntimeException('提现记录不存在'); } - if ((string)$withdraw['status'] !== 'auditing') { + if (!in_array((string)$withdraw['status'], ['auditing', 'paying'], true)) { throw new \RuntimeException('该提现记录已处理(当前状态:' . $withdraw['status'] . ')'); } - $outBillNo = self::makeOutBillNo($withdraw); + // 若当前单据卡在 paying 状态,先主动向微信查询是否已创建转账单 + if ((string)$withdraw['status'] === 'paying' && !empty($withdraw['out_bill_no'])) { + try { + $queryRes = self::query($withdraw['out_bill_no']); + if (!empty($queryRes['state'])) { + $state = self::applyResult($queryRes); + $withdrawState = (string)Db::name('fc_withdraw')->where('id', (int)$withdraw['id'])->value('status'); + return ['state' => $state, 'withdrawState' => $withdrawState]; + } + } catch (\Throwable $e) { + Log::record('提现记录 #' . $withdrawId . ' 处于 paying,微信查询补偿跳过:' . $e->getMessage(), 'notice'); + } + } + + $outBillNo = !empty($withdraw['out_bill_no']) ? $withdraw['out_bill_no'] : self::makeOutBillNo($withdraw); $now = time(); // 1) 先落「转账中」,防止重复点击造成重复转账 Db::transaction(function () use ($withdraw, $outBillNo, $now) { $locked = Db::name('fc_withdraw')->where('id', (int)$withdraw['id'])->lock(true)->find(); - if (!$locked || (string)$locked['status'] !== 'auditing') { + if (!$locked || !in_array((string)$locked['status'], ['auditing', 'paying'], true)) { throw new \RuntimeException('该提现记录已处理,请刷新后重试'); } Db::name('fc_withdraw')->where('id', (int)$locked['id'])->update([ @@ -208,10 +223,11 @@ class WxTransfer throw $e; } + $transferStatus = substr((string)$res['state'], 0, 32); Db::name('fc_withdraw')->where('id', (int)$withdraw['id'])->update([ - 'transfer_id' => $res['transfer_bill_no'], - 'package_info' => $res['package_info'], - 'transfer_status' => $res['state'], + 'transfer_id' => isset($res['transfer_bill_no']) ? (string)$res['transfer_bill_no'] : '', + 'package_info' => isset($res['package_info']) ? (string)$res['package_info'] : '', + 'transfer_status' => $transferStatus, 'transfer_time' => time(), ]); @@ -219,7 +235,8 @@ class WxTransfer $state = self::applyResult([ 'out_bill_no' => $outBillNo, 'state' => $res['state'], - 'transfer_bill_no' => $res['transfer_bill_no'], + 'transfer_bill_no' => isset($res['transfer_bill_no']) ? (string)$res['transfer_bill_no'] : '', + 'package_info' => isset($res['package_info']) ? (string)$res['package_info'] : '', 'fail_reason' => isset($res['fail_reason']) ? $res['fail_reason'] : '', ]); @@ -327,6 +344,7 @@ class WxTransfer $state = strtoupper(trim((string)(isset($data['state']) ? $data['state'] : ''))); $transferBillNo = isset($data['transfer_bill_no']) ? (string)$data['transfer_bill_no'] : ''; + $packageInfo = isset($data['package_info']) ? (string)$data['package_info'] : ''; $failReason = isset($data['fail_reason']) ? (string)$data['fail_reason'] : ''; $withdraw = Db::name('fc_withdraw')->where('out_bill_no', $outBillNo)->find(); @@ -448,12 +466,18 @@ class WxTransfer } // WAIT_USER_CONFIRM / ACCEPTED / PROCESSING:保持 paying,等待用户确认或回调 - Db::name('fc_withdraw')->where('id', (int)$withdraw['id'])->update([ - 'transfer_status' => $state, - 'transfer_id' => $transferBillNo, + $updateData = [ + 'transfer_status' => substr($state, 0, 32), 'transfer_time' => $now, 'updatetime' => $now, - ]); + ]; + if ($transferBillNo !== '') { + $updateData['transfer_id'] = $transferBillNo; + } + if ($packageInfo !== '') { + $updateData['package_info'] = $packageInfo; + } + Db::name('fc_withdraw')->where('id', (int)$withdraw['id'])->update($updateData); return 'paying'; } diff --git a/public/assets/js/backend/fc/userext.js b/public/assets/js/backend/fc/userext.js index 4317dc6..6bf4d36 100644 --- a/public/assets/js/backend/fc/userext.js +++ b/public/assets/js/backend/fc/userext.js @@ -27,14 +27,30 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu columns: [ [ {checkbox: true}, - {field: 'id', title: __('SerialNo'), operate: false, sortable: false, align: 'center', width: '70px', class: 'fc-serial', table: table, formatter: Format.serial}, - {field: 'nickname', title: '用户', operate: 'LIKE'}, + {field: 'id', title: __('SerialNo'), operate: false, sortable: false, align: 'center', width: '60px', class: 'fc-serial', table: table, formatter: Format.serial}, + {field: 'nickname', title: '用户昵称', operate: 'LIKE'}, {field: 'mobile', title: '手机号', operate: 'LIKE'}, + { + field: 'realname', title: '真实姓名(实名)', operate: 'LIKE', + formatter: function (value, row) { + if (!value) { + return ' 未填写'; + } + var isAuthed = parseInt(row.realname_status) === 1; + var icon = isAuthed ? ' ' : ' '; + return icon + '' + value + ''; + } + }, + { + field: 'realname_status', title: '实名状态', + searchList: {'0': '未认证', '1': '已认证'}, + formatter: Format.statusFormatter({'0': '未认证', '1': '已实名'}, {'0': 'default', '1': 'success'}) + }, { field: 'money', title: __('Money'), operate: false, formatter: function (value) { var n = parseFloat(value || 0); - return n > 0 ? '¥' + n.toFixed(2) : '¥0.00'; + return '¥' + n.toFixed(2) + ''; } }, { @@ -51,28 +67,31 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu {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: 'openid', title: __('Openid'), operate: 'LIKE', visible: false}, - { - field: 'realname_status', title: __('Realname_status'), visible: false, - searchList: {'0': __('未认证'), '1': __('已认证')}, - formatter: function (value) { - var m = {'0': __('未认证'), '1': __('已认证')}; - return m[value] !== undefined ? m[value] : '-'; - } - }, {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: 'post_quota_left', title: '剩余发布额度', operate: false, 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, buttons: [ + { + name: 'detail', + text: '详情', + title: function (row) { + return '用户详情 - ' + (row.nickname || ('UID:' + row.user_id)); + }, + classname: 'btn btn-xs btn-info btn-dialog', + icon: 'fa fa-user-circle-o', + url: 'fc/userext/detail', + extend: 'data-area=\'["960px", "780px"]\'' + }, { name: 'recharge', text: '充值', title: '余额充值', classname: 'btn btn-xs btn-warning btn-click', + icon: 'fa fa-money', click: function (options, row) { var nickname = row.nickname || ('用户#' + row.user_id); var balance = parseFloat(row.money || 0).toFixed(2); @@ -121,6 +140,9 @@ define(['jquery', 'bootstrap', 'backend', 'table', 'form', 'backend/format'], fu edit: function () { Controller.api.bindevent(); }, + detail: function () { + Controller.api.bindevent(); + }, api: { bindevent: function () { Form.api.bindevent($("form[role=form]")); diff --git a/tools/deploy_latest_changes.py b/tools/deploy_latest_changes.py new file mode 100644 index 0000000..373bbbb --- /dev/null +++ b/tools/deploy_latest_changes.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +部署最新修改至线上服务器 +变动文件: +- application/admin/controller/fc/Userext.php +- application/admin/view/fc/userext/edit.html +- application/admin/view/fc/userext/detail.html +- application/api/controller/Auth.php +- public/assets/js/backend/fc/userext.js +""" + +import os +import sys +import io +import paramiko + +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') + +HOST = "8.137.181.63" +PORT = 22 +USER = "root" +PASSWORD = "liyanxia@6688" +REMOTE_ROOT = "/www/wwwroot/fenleixinxi.gzyakj.com.cn" +LOCAL_ROOT = r"D:\Softwares\phpstudy_pro\WWW\fenleixinxi" + +SYNC_FILES = [ + "application/admin/controller/fc/Userext.php", + "application/admin/view/fc/userext/edit.html", + "application/admin/view/fc/userext/detail.html", + "application/api/controller/Auth.php", + "public/assets/js/backend/fc/userext.js", +] + +def ssh_exec(client, cmd): + stdin, stdout, stderr = client.exec_command(cmd) + out = stdout.read().decode("utf-8", errors="replace").strip() + err = stderr.read().decode("utf-8", errors="replace").strip() + return out, err + +def main(): + print("=== [1/3] 连接线上服务器 ===") + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + client.connect(HOST, port=PORT, username=USER, password=PASSWORD, timeout=30) + print(f"✓ 成功连接 {HOST}") + + sftp = client.open_sftp() + print("✓ SFTP 通道已建立") + + print("\n=== [2/3] 同步最新修改文件 ===") + for rel_path in SYNC_FILES: + local_path = os.path.join(LOCAL_ROOT, rel_path.replace("/", "\\")) + remote_path = f"{REMOTE_ROOT}/{rel_path}" + + if not os.path.exists(local_path): + print(f" ✗ 本地文件缺失: {local_path}") + continue + + # 确保远程目录存在 + remote_dir = os.path.dirname(remote_path) + ssh_exec(client, f"mkdir -p '{remote_dir}'") + + sftp.put(local_path, remote_path) + ssh_exec(client, f"chown www:www '{remote_path}'") + file_size = os.path.getsize(local_path) + print(f" ✓ 已同步: {rel_path} ({file_size} 字节)") + + sftp.close() + + print("\n=== [3/3] 清理缓存与服务自检 ===") + ssh_exec(client, f"rm -rf {REMOTE_ROOT}/runtime/temp/* {REMOTE_ROOT}/runtime/cache/*") + print("✓ 线上 ThinkPHP 缓存清理完成") + + # 接口健康自检 + check_urls = [ + ("API 信息流接口", "https://fenleixinxi.gzyakj.com.cn/api/v1/home/feed"), + ("API 分类接口", "https://fenleixinxi.gzyakj.com.cn/api/v1/home/categories"), + ("后台登录页面", "https://fenleixinxi.gzyakj.com.cn/s.php/index/login"), + ] + for name, url in check_urls: + cmd = f"curl -s -o /dev/null -w '%{{http_code}}' -k '{url}'" + code, _ = ssh_exec(client, cmd) + print(f" {name}: HTTP {code}") + + client.close() + print("\n✓ 最新修改已顺利部署至线上服务器!") + +if __name__ == "__main__": + main() diff --git a/tools/deploy_transfer_fixes.py b/tools/deploy_transfer_fixes.py new file mode 100644 index 0000000..96310b2 --- /dev/null +++ b/tools/deploy_transfer_fixes.py @@ -0,0 +1,97 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +部署最新全部修改至线上服务器 +文件清单: +- addons/epay/library/v3new/Yansongda/Pay/Shortcut/Wechat/MchTransferShortcut.php +- application/admin/controller/fc/Userext.php +- application/admin/controller/fc/Withdraw.php +- application/admin/view/fc/userext/edit.html +- application/admin/view/fc/userext/detail.html +- application/api/controller/Auth.php +- application/api/controller/Distribution.php +- application/common/library/WxTransfer.php +- public/assets/js/backend/fc/userext.js +""" + +import os +import sys +import io +import paramiko + +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') + +HOST = "8.137.181.63" +PORT = 22 +USER = "root" +PASSWORD = "liyanxia@6688" +REMOTE_ROOT = "/www/wwwroot/fenleixinxi.gzyakj.com.cn" +LOCAL_ROOT = r"D:\Softwares\phpstudy_pro\WWW\fenleixinxi" + +SYNC_FILES = [ + "addons/epay/library/v3new/Yansongda/Pay/Shortcut/Wechat/MchTransferShortcut.php", + "application/admin/controller/fc/Userext.php", + "application/admin/controller/fc/Withdraw.php", + "application/admin/view/fc/userext/edit.html", + "application/admin/view/fc/userext/detail.html", + "application/api/controller/Auth.php", + "application/api/controller/Distribution.php", + "application/common/library/WxTransfer.php", + "public/assets/js/backend/fc/userext.js", +] + +def ssh_exec(client, cmd): + stdin, stdout, stderr = client.exec_command(cmd) + out = stdout.read().decode("utf-8", errors="replace").strip() + err = stderr.read().decode("utf-8", errors="replace").strip() + return out, err + +def main(): + print("=== [1/3] 连接线上服务器 ===") + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + client.connect(HOST, port=PORT, username=USER, password=PASSWORD, timeout=30) + print(f"✓ 成功连接 {HOST}") + + sftp = client.open_sftp() + print("✓ SFTP 通道已建立") + + print("\n=== [2/3] 同步最新修改的代码文件 ===") + for rel_path in SYNC_FILES: + local_path = os.path.join(LOCAL_ROOT, rel_path.replace("/", "\\")) + remote_path = f"{REMOTE_ROOT}/{rel_path}" + + if not os.path.exists(local_path): + print(f" ✗ 本地文件缺失: {local_path}") + continue + + remote_dir = os.path.dirname(remote_path) + ssh_exec(client, f"mkdir -p '{remote_dir}'") + + sftp.put(local_path, remote_path) + ssh_exec(client, f"chown www:www '{remote_path}'") + file_size = os.path.getsize(local_path) + print(f" ✓ 已同步: {rel_path} ({file_size} 字节)") + + sftp.close() + + print("\n=== [3/3] 清理缓存与服务自检 ===") + ssh_exec(client, f"rm -rf {REMOTE_ROOT}/runtime/temp/* {REMOTE_ROOT}/runtime/cache/*") + print("✓ 线上 ThinkPHP 缓存清理完成") + + # 接口健康自检 + check_urls = [ + ("API 信息流接口", "https://fenleixinxi.gzyakj.com.cn/api/v1/home/feed"), + ("API 分类接口", "https://fenleixinxi.gzyakj.com.cn/api/v1/home/categories"), + ("后台登录页面", "https://fenleixinxi.gzyakj.com.cn/s.php/index/login"), + ] + for name, url in check_urls: + cmd = f"curl -s -o /dev/null -w '%{{http_code}}' -k '{url}'" + code, _ = ssh_exec(client, cmd) + print(f" {name}: HTTP {code}") + + client.close() + print("\n✓ 最新代码已全部部署至线上服务器!") + +if __name__ == "__main__": + main() diff --git a/tools/exec_alter_table.py b/tools/exec_alter_table.py new file mode 100644 index 0000000..06b6247 --- /dev/null +++ b/tools/exec_alter_table.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""在远程和本地数据库执行 ALTER TABLE 语句""" + +import paramiko +import pymysql +import sys +import io + +sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') + +HOST = "8.137.181.63" +PORT = 22 +USER = "root" +PASSWORD = "liyanxia@6688" +DB_USER = "fenleixinxi" +DB_PASS = "fenleixinxi" +DB_NAME = "fenleixinxi" + +LOCAL_DB_HOST = "127.0.0.1" +LOCAL_DB_PORT = 3307 + +SQL = "ALTER TABLE `fa_fc_withdraw` MODIFY COLUMN `transfer_status` varchar(32) NOT NULL DEFAULT '' COMMENT 'WAIT_USER_CONFIRM/ACCEPTED/PROCESSING/SUCCESS/FAIL/CANCELLED';" + +def main(): + print("=== [1/2] 线上数据库执行 ALTER TABLE ===") + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(HOST, port=PORT, username=USER, password=PASSWORD, timeout=20) + + # 1. 查看线上执行前字段定义 + cmd_chk_before = f'mysql -u{DB_USER} -p{DB_PASS} {DB_NAME} -e "SHOW FULL COLUMNS FROM fa_fc_withdraw LIKE \'transfer_status\';" 2>/dev/null' + _, stdout, _ = ssh.exec_command(cmd_chk_before) + print("线上执行前状态:\n" + stdout.read().decode('utf-8', errors='replace').strip()) + + # 2. 执行 ALTER TABLE + cmd_exec = f'mysql -u{DB_USER} -p{DB_PASS} {DB_NAME} -e "{SQL}" 2>/dev/null' + _, stdout, stderr = ssh.exec_command(cmd_exec) + err = stderr.read().decode('utf-8', errors='replace').strip() + if err and "error" in err.lower(): + print(f"✗ 线上执行报错: {err}") + else: + print("✓ 线上执行成功!") + + # 3. 查看线上执行后字段定义 + cmd_chk_after = f'mysql -u{DB_USER} -p{DB_PASS} {DB_NAME} -e "SHOW FULL COLUMNS FROM fa_fc_withdraw LIKE \'transfer_status\';" 2>/dev/null' + _, stdout, _ = ssh.exec_command(cmd_chk_after) + print("\n线上执行后状态:\n" + stdout.read().decode('utf-8', errors='replace').strip()) + + ssh.close() + + print("\n=== [2/2] 本地开发数据库执行 ALTER TABLE (同步保持一致) ===") + try: + conn = pymysql.connect( + host=LOCAL_DB_HOST, + port=LOCAL_DB_PORT, + user=DB_USER, + password=DB_PASS, + database=DB_NAME, + charset="utf8mb4", + autocommit=True + ) + with conn.cursor() as cur: + cur.execute(SQL) + cur.execute("SHOW FULL COLUMNS FROM fa_fc_withdraw LIKE 'transfer_status';") + row = cur.fetchone() + print(f"✓ 本地数据库执行成功: 字段={row[0]}, 类型={row[1]}, 注释={row[8]}") + conn.close() + except Exception as e: + print(f"- 本地执行提示: {e}") + + print("\n✓ 全部执行与验证完成!") + +if __name__ == "__main__": + main() diff --git a/uniapp/pages/distribution/index.vue b/uniapp/pages/distribution/index.vue index 06b16e3..0c5461c 100644 --- a/uniapp/pages/distribution/index.vue +++ b/uniapp/pages/distribution/index.vue @@ -374,16 +374,29 @@ {{ money(overview.withdrawable) }} - - - + + + + 收款人真实姓名 + + + 已实名认证 + + 未实名 + + + + + + 资金直转此姓名对应的微信零钱,请务必核对一致 + @@ -1548,6 +1561,7 @@ export default { return } this.withdrawAmount = '' + this.realnameInput = this.realname || '' this.showWithdraw = true }, @@ -1557,22 +1571,41 @@ export default { */ confirmTransfer(w) { if (!w || !w.packageInfo) { - uni.showToast({ title: '转账处理中,请在微信消息中确认收款', icon: 'none' }) + uni.showToast({ title: '转账处理中,请稍后刷新重试', icon: 'none' }) return } - if (typeof uni === 'undefined' || !uni.requestMerchantTransfer) { - uni.showToast({ title: '请在微信消息中确认收款', icon: 'none' }) + // 优先使用微信原生 API(wx.requestMerchantTransfer 为微信基础库 3.3.0+ 商家转账确认专属 API) + const api = (typeof wx !== 'undefined' && typeof wx.requestMerchantTransfer === 'function') + ? wx.requestMerchantTransfer + : (typeof uni !== 'undefined' && typeof uni.requestMerchantTransfer === 'function' ? uni.requestMerchantTransfer : null) + + if (!api) { + uni.showToast({ title: '当前微信版本过低,请升级微信后重试', icon: 'none' }) return } - uni.requestMerchantTransfer({ - mchId: (this.overview && this.overview.transferMchId) || '', - appId: (this.overview && this.overview.transferAppId) || '', - package: w.packageInfo, - success: () => { - uni.showToast({ title: '已确认,等待到账', icon: 'none' }) + + const mchId = String((w && w.transferMchId) || (this.overview && this.overview.transferMchId) || '1118898018') + const appId = String((w && w.transferAppId) || (this.overview && this.overview.transferAppId) || 'wxbfcb778f3019cff8') + const pkg = String(w.packageInfo) + + api({ + mchId: mchId, + appId: appId, + package: pkg, + success: (res) => { + console.log('[Transfer] 确认收款成功:', res) + uni.showToast({ title: '已确认,等待到账', icon: 'success' }) setTimeout(() => this.refreshAfterWithdraw(), 1500) }, - fail: () => uni.showToast({ title: '已取消确认', icon: 'none' }), + fail: (err) => { + console.warn('[Transfer] 确认收款回调异常:', err) + const msg = (err && (err.errMsg || err.message)) || '' + if (msg.indexOf('cancel') !== -1) { + uni.showToast({ title: '您取消了确认收款', icon: 'none' }) + } else { + uni.showToast({ title: msg || '调起确认失败,请重试', icon: 'none', duration: 3000 }) + } + }, }) }, @@ -1614,17 +1647,19 @@ export default { this.submitting = true try { - if (!this.realname) { - const name = String(this.realnameInput || '').trim() - if (!/^[\u4e00-\u9fa5·]{2,20}$/.test(name)) { - uni.showToast({ title: '请填写 2-20 位中文真实姓名', icon: 'none' }) - return - } + const name = String(this.realnameInput || this.realname || '').trim() + if (!name || !/^[\u4e00-\u9fa5·]{2,20}$/.test(name)) { + uni.showToast({ title: '请填写 2-20 位中文真实姓名', icon: 'none' }) + this.submitting = false + return + } + if (name !== this.realname) { try { await authApi.saveRealname({ realname: name }) this.realname = name } catch (e) { - uni.showToast({ title: e.message || '姓名保存失败', icon: 'none' }) + uni.showToast({ title: (e && e.message) || '姓名保存失败', icon: 'none' }) + this.submitting = false return } } @@ -2400,6 +2435,48 @@ export default { font-weight: 700; color: #04301E; } +.dist__sheet-recipient { + margin-top: 24rpx; +} +.dist__sheet-recipient-head { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10rpx; +} +.dist__sheet-recipient-title { + font-size: 26rpx; + font-weight: 600; + color: #1E293B; +} +.dist__sheet-recipient-tag { + display: flex; + align-items: center; + gap: 6rpx; + padding: 4rpx 12rpx; + border-radius: 8rpx; + background: #E8FAF0; + color: #00A854; + font-size: 20rpx; + font-weight: 600; +} +.dist__sheet-recipient-tag--unauth { + background: #F1F5F9; + color: #94A3B8; +} +.dist__sheet-recipient-tag-icon { + width: 20rpx; + height: 20rpx; +} +.dist__sheet-recipient-hint { + margin-top: 8rpx; + font-size: 22rpx; + color: #F5A623; +} +.dist__sheet-field--name { + margin-top: 0; + padding: 16rpx 20rpx; +} .dist__sheet-field { display: flex; align-items: center; diff --git a/uniapp/pages/mine/profile.vue b/uniapp/pages/mine/profile.vue index 7a41698..096feb4 100644 --- a/uniapp/pages/mine/profile.vue +++ b/uniapp/pages/mine/profile.vue @@ -48,6 +48,30 @@ 可直接点击输入框自动填入微信昵称 + + + + 收款人真实姓名 + + 已实名认证 + + + 未实名 + + + + + + 用于微信零钱提现核验,须与微信支付实名姓名完全一致 + + 绑定手机号 @@ -105,6 +129,7 @@ export default { previewAvatar: '', form: { nickname: '', + realname: '', }, } }, @@ -175,6 +200,7 @@ export default { initData() { if (this.userInfo) { this.form.nickname = this.userInfo.nickname || '' + this.form.realname = this.userInfo.realname || '' } }, /** @@ -217,6 +243,11 @@ export default { uni.showToast({ title: '请输入用户昵称', icon: 'none' }) return } + const realname = (this.form.realname || '').trim() + if (realname && !/^[\u4e00-\u9fa5·]{2,20}$/.test(realname)) { + uni.showToast({ title: '真实姓名需为2-20位中文', icon: 'none' }) + return + } this.saving = true try { @@ -231,6 +262,7 @@ export default { const payload = { nickname, + realname, } if (avatarUrl) { payload.avatar = avatarUrl @@ -357,6 +389,41 @@ export default { margin-bottom: 12rpx; } +.profile__label-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 12rpx; + + .profile__label { + margin-bottom: 0; + } +} + +.profile__tag { + padding: 4rpx 14rpx; + border-radius: 8rpx; + display: inline-flex; + align-items: center; + + &--verified { + background: #e8fbf1; + .profile__tag-text { + font-size: 22rpx; + font-weight: 600; + color: #00a854; + } + } + + &--pending { + background: #f1f5f9; + .profile__tag-text { + font-size: 22rpx; + color: #94a3b8; + } + } +} + .profile__input-wrap { width: 100%; height: 88rpx;