-
+
+
+
+
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;