diff --git a/.gitignore b/.gitignore index a792529..993e10c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ antdui-demo/ +dujiaoka-master/ +Vmianqian/ .vs .idea \ No newline at end of file diff --git a/Form1.cs b/Form1.cs index cbd0598..105790f 100644 --- a/Form1.cs +++ b/Form1.cs @@ -1,5 +1,7 @@ using System.Net; using System.Net.Http.Json; +using System.Diagnostics; +using System.Text.RegularExpressions; using System.Runtime.InteropServices; using System.Security.Cryptography; using System.Text; @@ -9,6 +11,7 @@ using AntdUI; using MailKit.Net.Smtp; using MailKit.Security; using MimeKit; +using System.Windows.Automation; using WinLabel = System.Windows.Forms.Label; using WinPanel = System.Windows.Forms.Panel; using WinTextBox = System.Windows.Forms.TextBox; @@ -27,6 +30,7 @@ namespace Vmianqian private static extern nint SendMessage(nint hWnd, int msg, nint wParam, nint lParam); private readonly string _configFilePath = Path.Combine(AppContext.BaseDirectory, "appsettings.client.json"); + private readonly string _pendingOrdersFilePath = Path.Combine(AppContext.BaseDirectory, "pending-orders.client.json"); private readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true, PropertyNameCaseInsensitive = true }; private readonly HttpClient _httpClient = new(); private readonly System.Windows.Forms.Timer _runtimeTimer = new(); @@ -36,7 +40,20 @@ namespace Vmianqian private HttpListener? _httpListener; private CancellationTokenSource? _listenerCancellationTokenSource; private ClientConfig _config = new(); + private List _pendingOrders = new(); private bool _isSynchronizingNavigation; + private string _wechatMonitorMode = string.Empty; + private CancellationTokenSource? _wechatHookCts; + private CancellationTokenSource? _wechatProtocolCts; + private CancellationTokenSource? _wechatSidCaptureCts; + private readonly HashSet _wechatHookSeen = new(StringComparer.Ordinal); + private readonly HashSet _wechatProtocolSeen = new(StringComparer.Ordinal); + private string _wechatProtocolLastTransId = string.Empty; + private long _wechatProtocolLastCreateTime; + private int? _wechatProtocolLastPendingCount; + private int? _wechatProtocolLastHomeType; + private DateTime _wechatHookLastDiagAt = DateTime.MinValue; + private bool _wechatHookFoundLedgerAnchor; private AntdUI.PageHeader titlebar = null!; private AntdUI.PageHeader bottomBar = null!; @@ -73,6 +90,9 @@ namespace Vmianqian private AntdUI.Label lblSmtpPortTitle = null!; private AntdUI.Label lblNotifyEmailTitle = null!; private AntdUI.Label lblEmailAuthCodeTitle = null!; + private AntdUI.Label lblWechatSidTitle = null!; + private AntdUI.Label lblWechatFrequencyTitle = null!; + private AntdUI.Label lblWechatPollingTitle = null!; private AntdUI.Label lblHeartbeatDesc = null!; private AntdUI.Label lblMemberPlaceholder = null!; private AntdUI.Switch chkHeartbeatEnabled = null!; @@ -82,6 +102,11 @@ namespace Vmianqian private AntdUI.Input txtWechatPath = null!; private AntdUI.Input txtWechatId = null!; + private AntdUI.Button btnSelectWechatPath = null!; + private AntdUI.Button btnWechatHookStart = null!; + private AntdUI.Button btnWechatProtocolStart = null!; + private AntdUI.Button btnWechatSidAuto = null!; + private AntdUI.Button btnClearWechatLog = null!; private NumericUpDown numWechatInterval = null!; private AntdUI.Checkbox chkWheel = null!; @@ -405,11 +430,45 @@ namespace Vmianqian private void LayoutWechatPage() { - LayoutPageCards(pageWechat, 20); - + const int margin = 20; + const int gap = 16; + var hookCard = FindCard(pageWechat, "wechat-hook"); + var protocolCard = FindCard(pageWechat, "wechat-protocol"); var gridCard = FindCard(pageWechat, "wechat-log"); - if (gridCard != null) + + if (hookCard != null && protocolCard != null) { + var totalWidth = Math.Max(0, pageWechat.ClientSize.Width - margin * 2 - gap); + var hookWidth = totalWidth / 2; + var protocolWidth = totalWidth - hookWidth; + hookCard.Bounds = new Rectangle(margin, 20, hookWidth, 240); + protocolCard.Bounds = new Rectangle(hookCard.Right + gap, 20, protocolWidth, 240); + + var hookPathWidth = Math.Max(140, hookCard.ClientSize.Width - 24 - 24 - btnSelectWechatPath.Width - 12); + txtWechatPath.Width = hookPathWidth; + btnSelectWechatPath.Location = new Point(txtWechatPath.Left + txtWechatPath.Width + 12, txtWechatPath.Top); + btnWechatHookStart.Size = new Size(120, 36); + btnWechatHookStart.Location = new Point(24, hookCard.ClientSize.Height - btnWechatHookStart.Height - 16); + + lblWechatSidTitle.Location = new Point(24, 54); + var sidWidth = Math.Max(180, protocolCard.ClientSize.Width - 48 - btnWechatSidAuto.Width - 12); + txtWechatId.Width = sidWidth; + btnWechatSidAuto.Location = new Point(txtWechatId.Left + txtWechatId.Width + 12, txtWechatId.Top); + var halfWidth = Math.Max(160, (sidWidth - 12) / 2); + var rightColX = 24 + halfWidth + 12; + lblWechatFrequencyTitle.Location = new Point(24, 122); + numWechatInterval.Location = new Point(24, 150); + lblWechatPollingTitle.Location = new Point(rightColX, 122); + chkWheel.Location = new Point(rightColX, 150); + btnWechatProtocolStart.Size = new Size(120, 36); + btnWechatProtocolStart.Location = new Point(24, protocolCard.ClientSize.Height - btnWechatProtocolStart.Height - 16); + } + + if (gridCard != null && hookCard != null) + { + gridCard.Left = margin; + gridCard.Width = Math.Max(0, pageWechat.ClientSize.Width - margin * 2); + gridCard.Top = hookCard.Bottom + margin; var bottom = pageWechat.ClientSize.Height - 20; gridCard.Height = Math.Max(260, bottom - gridCard.Top); @@ -417,6 +476,11 @@ namespace Vmianqian { gridWechatLogs.Size = new Size(Math.Max(240, gridCard.ClientSize.Width - 50), Math.Max(120, gridCard.ClientSize.Height - 76)); } + + if (btnClearWechatLog != null) + { + btnClearWechatLog.Location = new Point(Math.Max(24, gridCard.ClientSize.Width - btnClearWechatLog.Width - 24), 14); + } } } @@ -631,51 +695,101 @@ namespace Vmianqian private void BuildWechatPage() { - var card = CreateCardPanel(new Rectangle(20, 20, 1080, 150)); - card.Tag = "wechat-config"; - card.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; - - card.Controls.Add(CreateTitleLabel("微信安装路径", 24, 20)); + var hookCard = CreateCardPanel(new Rectangle(20, 20, 532, 240)); + hookCard.Tag = "wechat-hook"; + hookCard.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + hookCard.Controls.Add(CreateTitleLabel("Hook 功能", 24, 18)); + hookCard.Controls.Add(CreateTitleLabel("微信安装路径", 24, 54)); txtWechatPath = CreateInput(24, 48, 390, "微信.exe 路径"); - card.Controls.Add(txtWechatPath); + txtWechatPath.Location = new Point(24, 82); + txtWechatPath.Width = 280; + hookCard.Controls.Add(txtWechatPath); + btnSelectWechatPath = new AntdUI.Button + { + Text = "选择路径", + Type = TTypeMini.Primary, + Ghost = true, + Location = new Point(316, 82), + Size = new Size(92, 36) + }; + btnSelectWechatPath.Click += btnSelectWechatPath_Click; + hookCard.Controls.Add(btnSelectWechatPath); + btnWechatHookStart = new AntdUI.Button + { + Text = "开始监听", + Type = TTypeMini.Primary, + Location = new Point(24, 128), + Size = new Size(120, 36) + }; + btnWechatHookStart.Click += btnWechatHookStart_Click; + hookCard.Controls.Add(btnWechatHookStart); - card.Controls.Add(CreateTitleLabel("微信 SID", 440, 20)); - txtWechatId = CreateInput(440, 48, 220, "可选"); - card.Controls.Add(txtWechatId); + var protocolCard = CreateCardPanel(new Rectangle(568, 20, 532, 240)); + protocolCard.Tag = "wechat-protocol"; + protocolCard.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right; + protocolCard.Controls.Add(CreateTitleLabel("协议功能", 24, 18)); - card.Controls.Add(CreateTitleLabel("轮询频率(秒)", 690, 20)); + lblWechatSidTitle = CreateTitleLabel("微信 SID", 24, 54); + protocolCard.Controls.Add(lblWechatSidTitle); + txtWechatId = CreateInput(24, 82, 356, "可选"); + protocolCard.Controls.Add(txtWechatId); + btnWechatSidAuto = new AntdUI.Button + { + Text = "自动获取", + Type = TTypeMini.Primary, + Ghost = true, + Location = new Point(392, 82), + Size = new Size(92, 36) + }; + btnWechatSidAuto.Click += btnWechatSidAuto_Click; + protocolCard.Controls.Add(btnWechatSidAuto); + + lblWechatFrequencyTitle = CreateTitleLabel("监控频率(秒)", 24, 122); + protocolCard.Controls.Add(lblWechatFrequencyTitle); numWechatInterval = new NumericUpDown { - Location = new Point(690, 51), + Location = new Point(24, 150), Size = new Size(96, 23), Minimum = 1, Maximum = 3600, Value = 5 }; - card.Controls.Add(numWechatInterval); + protocolCard.Controls.Add(numWechatInterval); + lblWechatPollingTitle = CreateTitleLabel("接口轮询", 274, 122); + protocolCard.Controls.Add(lblWechatPollingTitle); chkWheel = new AntdUI.Checkbox { - Text = "启用轮询", - Location = new Point(820, 50), + Text = "接口轮询", + Location = new Point(274, 150), AutoSize = true }; - card.Controls.Add(chkWheel); - - var desc = new AntdUI.Label + protocolCard.Controls.Add(chkWheel); + btnWechatProtocolStart = new AntdUI.Button { - Text = "后续这里接入微信真实到账监听逻辑,目前保留参数配置与回调结果展示。", - AutoSize = false, - Location = new Point(24, 102), - Size = new Size(960, 24), - ForeColor = Color.DimGray + Text = "开始监听", + Type = TTypeMini.Primary, + Location = new Point(24, 196), + Size = new Size(120, 36) }; - card.Controls.Add(desc); + btnWechatProtocolStart.Click += btnWechatProtocolStart_Click; + protocolCard.Controls.Add(btnWechatProtocolStart); - var gridCard = CreateCardPanel(new Rectangle(20, 190, 1080, 540)); + var gridCard = CreateCardPanel(new Rectangle(20, 280, 1080, 450)); gridCard.Tag = "wechat-log"; gridCard.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; gridCard.Controls.Add(CreateTitleLabel("微信监听记录", 24, 18)); + btnClearWechatLog = new AntdUI.Button + { + Text = "清空", + Type = TTypeMini.Primary, + Ghost = true, + Location = new Point(980, 14), + Size = new Size(72, 30), + Anchor = AnchorStyles.Top | AnchorStyles.Right + }; + btnClearWechatLog.Click += (_, _) => gridWechatLogs.Rows.Clear(); + gridCard.Controls.Add(btnClearWechatLog); gridWechatLogs = CreateWechatGrid(); gridWechatLogs.Location = new Point(24, 50); @@ -683,8 +797,10 @@ namespace Vmianqian gridWechatLogs.Anchor = AnchorStyles.Top | AnchorStyles.Bottom | AnchorStyles.Left | AnchorStyles.Right; gridCard.Controls.Add(gridWechatLogs); - pageWechat.Controls.Add(card); + pageWechat.Controls.Add(hookCard); + pageWechat.Controls.Add(protocolCard); pageWechat.Controls.Add(gridCard); + UpdateWechatMonitorButtons(); } private void BuildAlipayPage() @@ -909,6 +1025,9 @@ namespace Vmianqian { StopListener(); StopHeartbeat(); + StopWechatHook(); + StopWechatProtocol(); + StopWechatSidCapture(); _runtimeTimer.Stop(); _runtimeTimer.Dispose(); _heartbeatTimer.Dispose(); @@ -1009,11 +1128,14 @@ namespace Vmianqian if (!File.Exists(_configFilePath)) { _config = new ClientConfig(); - return; + } + else + { + var json = File.ReadAllText(_configFilePath, Encoding.UTF8); + _config = JsonSerializer.Deserialize(json, _jsonOptions) ?? new ClientConfig(); } - var json = File.ReadAllText(_configFilePath, Encoding.UTF8); - _config = JsonSerializer.Deserialize(json, _jsonOptions) ?? new ClientConfig(); + LoadPendingOrders(); } private void SaveConfig() @@ -1022,6 +1144,31 @@ namespace Vmianqian File.WriteAllText(_configFilePath, json, Encoding.UTF8); } + private void LoadPendingOrders() + { + if (!File.Exists(_pendingOrdersFilePath)) + { + _pendingOrders = new List(); + return; + } + + try + { + var json = File.ReadAllText(_pendingOrdersFilePath, Encoding.UTF8); + _pendingOrders = JsonSerializer.Deserialize>(json, _jsonOptions) ?? new List(); + } + catch + { + _pendingOrders = new List(); + } + } + + private void SavePendingOrders() + { + var json = JsonSerializer.Serialize(_pendingOrders, _jsonOptions); + File.WriteAllText(_pendingOrdersFilePath, json, Encoding.UTF8); + } + private void BindConfigToUi() { txtServerUrl.Text = _config.ServerUrl; @@ -1042,6 +1189,7 @@ namespace Vmianqian numAlipayInterval.Value = Math.Min(Math.Max(_config.AlipayIntervalSeconds, 1), 3600); chkWheel.Checked = _config.EnableWheelPolling; chkHeartbeatEnabled.Checked = _config.EnableHeartbeat; + _config.WechatApiVersion = string.IsNullOrWhiteSpace(_config.WechatApiVersion) ? "7.10.1" : _config.WechatApiVersion.Trim(); } private void SaveUiToConfig() @@ -1102,6 +1250,1036 @@ namespace Vmianqian } } + private void btnSelectWechatPath_Click(object? sender, EventArgs e) + { + using var dialog = new OpenFileDialog + { + Title = "选择微信程序路径", + Filter = "微信程序|WeChat.exe;Weixin.exe;WeChatAppEx.exe|可执行文件|*.exe|所有文件|*.*", + CheckFileExists = true, + Multiselect = false + }; + + if (dialog.ShowDialog(this) != DialogResult.OK) + { + return; + } + + txtWechatPath.Text = dialog.FileName; + SaveUiToConfig(); + SaveConfig(); + Log($"微信安装路径已保存:{dialog.FileName}"); + } + + private async void btnWechatSidAuto_Click(object? sender, EventArgs e) + { + if (_wechatSidCaptureCts != null) + { + StopWechatSidCapture(); + Log("SID 捕获已停止。"); + return; + } + + try + { + Log("SID 捕获已启动:请现在打开微信收款助手/收款小账本页面。"); + StartWechatSidCapture(); + } + catch (Exception ex) + { + Log($"自动获取 SID 异常:{ex.Message}"); + StopWechatSidCapture(); + } + } + + private void StartWechatSidCapture() + { + StopWechatSidCapture(); + _wechatSidCaptureCts = new CancellationTokenSource(); + var token = _wechatSidCaptureCts.Token; + btnWechatSidAuto.Text = "停止捕获"; + btnWechatSidAuto.Type = TTypeMini.Error; + btnWechatSidAuto.Loading = true; + + _ = Task.Run(async () => + { + while (!token.IsCancellationRequested) + { + try + { + if (TryExtractSidFromClipboard(out var clipSid, out var clipVersion)) + { + BeginInvoke(() => ApplyCapturedSid(clipSid, clipVersion, "剪贴板")); + return; + } + + var result = TryExtractSidFromWechatLocalFiles(); + if (!string.IsNullOrWhiteSpace(result.Sid)) + { + BeginInvoke(() => ApplyCapturedSid(result.Sid, result.Version, result.Source)); + return; + } + + await Task.Delay(TimeSpan.FromSeconds(2), token); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + BeginInvoke(() => Log($"SID 捕获轮询异常:{ex.Message}")); + await Task.Delay(TimeSpan.FromSeconds(2), token); + } + } + }, token); + } + + private void StopWechatSidCapture() + { + try { _wechatSidCaptureCts?.Cancel(); } catch { } + try { _wechatSidCaptureCts?.Dispose(); } catch { } + _wechatSidCaptureCts = null; + + if (btnWechatSidAuto != null) + { + btnWechatSidAuto.Text = "自动获取"; + btnWechatSidAuto.Type = TTypeMini.Primary; + btnWechatSidAuto.Ghost = true; + btnWechatSidAuto.Loading = false; + btnWechatSidAuto.Enabled = true; + } + } + + private void ApplyCapturedSid(string sid, string version, string source) + { + txtWechatId.Text = sid; + SaveUiToConfig(); + _config.WechatApiVersion = string.IsNullOrWhiteSpace(version) ? _config.WechatApiVersion : version; + SaveConfig(); + Log($"已捕获 SID:{sid},v={_config.WechatApiVersion},来源={source}"); + StopWechatSidCapture(); + } + + private void btnWechatHookStart_Click(object? sender, EventArgs e) + { + if (string.Equals(_wechatMonitorMode, "hook", StringComparison.Ordinal)) + { + StopWechatHook(); + _wechatMonitorMode = string.Empty; + UpdateWechatMonitorButtons(); + UpdateWechatStatusUi(); + Log("Hook 监听已停止。"); + return; + } + + try + { + SaveUiToConfig(); + SaveConfig(); + if (!string.IsNullOrWhiteSpace(_wechatMonitorMode)) + { + StopWechatMonitoring(); + } + + if (string.IsNullOrWhiteSpace(txtWechatPath?.Text) || !File.Exists(txtWechatPath.Text.Trim())) + { + throw new InvalidOperationException("请先选择正确的微信程序路径(WeChat.exe/Weixin.exe/WeChatAppEx.exe)。"); + } + + StartWechatHook(); + _wechatMonitorMode = "hook"; + UpdateWechatMonitorButtons(); + UpdateWechatStatusUi(); + Log("Hook 监听已启动(将用于监听微信通知/收款小账本)。"); + } + catch (Exception ex) + { + Log($"Hook 监听启动失败:{ex.Message}"); + MessageBox.Show(ex.Message, "启动失败", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void btnWechatProtocolStart_Click(object? sender, EventArgs e) + { + if (string.Equals(_wechatMonitorMode, "protocol", StringComparison.Ordinal)) + { + StopWechatProtocol(); + _wechatMonitorMode = string.Empty; + UpdateWechatMonitorButtons(); + UpdateWechatStatusUi(); + Log("协议监听已停止。"); + return; + } + + try + { + SaveUiToConfig(); + SaveConfig(); + if (!string.IsNullOrWhiteSpace(_wechatMonitorMode)) + { + StopWechatMonitoring(); + } + + if (string.IsNullOrWhiteSpace(_config.WechatSid)) + { + throw new InvalidOperationException("请先填写或自动获取微信 SID。"); + } + + StartWechatProtocol(); + _wechatMonitorMode = "protocol"; + UpdateWechatMonitorButtons(); + UpdateWechatStatusUi(); + Log($"协议监听已启动。SID={_config.WechatSid},v={_config.WechatApiVersion}"); + } + catch (Exception ex) + { + Log($"协议监听启动失败:{ex.Message}"); + MessageBox.Show(ex.Message, "启动失败", MessageBoxButtons.OK, MessageBoxIcon.Error); + } + } + + private void UpdateWechatMonitorButtons() + { + if (btnWechatHookStart == null || btnWechatProtocolStart == null) + { + return; + } + + var hookActive = string.Equals(_wechatMonitorMode, "hook", StringComparison.Ordinal); + var protocolActive = string.Equals(_wechatMonitorMode, "protocol", StringComparison.Ordinal); + + btnWechatHookStart.Text = hookActive ? "停止监听" : "开始监听"; + btnWechatHookStart.Type = hookActive ? TTypeMini.Error : TTypeMini.Primary; + btnWechatProtocolStart.Text = protocolActive ? "停止监听" : "开始监听"; + btnWechatProtocolStart.Type = protocolActive ? TTypeMini.Error : TTypeMini.Primary; + } + + private void StopWechatMonitoring() + { + if (string.Equals(_wechatMonitorMode, "hook", StringComparison.Ordinal)) + { + StopWechatHook(); + } + else if (string.Equals(_wechatMonitorMode, "protocol", StringComparison.Ordinal)) + { + StopWechatProtocol(); + } + + UpdateWechatStatusUi(); + } + + private void StartWechatHook() + { + StopWechatHook(); + _wechatHookSeen.Clear(); + _wechatHookFoundLedgerAnchor = false; + _wechatHookLastDiagAt = DateTime.MinValue; + _wechatHookCts = new CancellationTokenSource(); + var token = _wechatHookCts.Token; + + _ = Task.Run(async () => + { + while (!token.IsCancellationRequested) + { + try + { + await PollWechatLedgerOnceAsync(token); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + Log($"Hook 轮询异常:{ex.Message}"); + } + + var delaySeconds = Math.Min(Math.Max(_config.WechatIntervalSeconds, 1), 60); + try + { + await Task.Delay(TimeSpan.FromSeconds(delaySeconds), token); + } + catch (OperationCanceledException) + { + break; + } + } + }, token); + } + + private void StopWechatHook() + { + try { _wechatHookCts?.Cancel(); } catch { } + try { _wechatHookCts?.Dispose(); } catch { } + _wechatHookCts = null; + } + + private async Task PollWechatLedgerOnceAsync(CancellationToken token) + { + token.ThrowIfCancellationRequested(); + + var processes = Process.GetProcessesByName("Weixin") + .Concat(Process.GetProcessesByName("WeChat")) + .Concat(Process.GetProcessesByName("WeChatAppEx")) + .ToArray(); + if (processes.Length == 0) + { + return; + } + + foreach (var process in processes) + { + if (process.MainWindowHandle == IntPtr.Zero) + { + continue; + } + + AutomationElement? window = null; + try + { + window = AutomationElement.FromHandle(process.MainWindowHandle); + } + catch + { + continue; + } + + if (window == null) + { + continue; + } + + // 优先收敛到“收款小账本”区域附近,降低噪声与误报 + var scanRoot = FindWechatLedgerScanRoot(window) ?? window; + if (!ReferenceEquals(scanRoot, window)) + { + _wechatHookFoundLedgerAnchor = true; + } + + // 双通道:小账本区域 + 整窗聊天文本(微信收款助手/微信支付) + var texts = ExtractAutomationText(scanRoot, max: 2500); + var windowTexts = ExtractAutomationText(window, max: 3000); + if (windowTexts.Count > 0) + { + texts.AddRange(windowTexts); + } + + texts = texts + .Where(v => !string.IsNullOrWhiteSpace(v)) + .Distinct(StringComparer.Ordinal) + .ToList(); + var moneyCandidates = 0; + var keywordSamples = new List(); + foreach (var t in texts) + { + token.ThrowIfCancellationRequested(); + if (t.Contains('¥') || t.Contains('¥')) + { + moneyCandidates++; + } + + if (ContainsLedgerKeyword(t) && keywordSamples.Count < 8) + { + keywordSamples.Add(t.Length > 40 ? t[..40] : t); + } + + if (TryParseWechatLedgerLine(t, out var amount, out var remark)) + { + var key = $"{amount:0.00}|{remark}|{t}"; + var hash = CreateMd5(key); + if (_wechatHookSeen.Add(hash)) + { + await OnWechatPaymentDetectedAsync(amount, remark, raw: t); + } + } + } + + // 低频诊断:告诉你扫描到没有包含金额的文本/是否定位到小账本锚点 + if ((DateTime.Now - _wechatHookLastDiagAt).TotalSeconds >= 5) + { + _wechatHookLastDiagAt = DateTime.Now; + if (!_wechatHookFoundLedgerAnchor) + { + Log("Hook 诊断:未定位到“收款小账本”区域,建议打开并停留在收款小账本页面再试。"); + } + else if (keywordSamples.Count > 0) + { + Log($"Hook 诊断:已命中关键词样本 {string.Join(" | ", keywordSamples.Distinct())}"); + } + else if (moneyCandidates == 0) + { + Log("Hook 诊断:已扫描微信窗口,但未命中“微信支付收款/收款到账/¥/¥/元”等文本。"); + } + } + } + } + + private static AutomationElement? FindWechatLedgerScanRoot(AutomationElement window) + { + // UIA 不支持 contains 条件,只能遍历找关键词命中 + var walker = TreeWalker.RawViewWalker; + var stack = new Stack(); + stack.Push(window); + while (stack.Count > 0) + { + var current = stack.Pop(); + if (IsLedgerAnchor(current)) + { + return current; + } + + AutomationElement? child = null; + try { child = walker.GetFirstChild(current); } catch { child = null; } + while (child != null) + { + stack.Push(child); + try { child = walker.GetNextSibling(child); } catch { break; } + } + } + + return null; + } + + private static bool IsLedgerAnchor(AutomationElement element) + { + foreach (var text in ExtractElementTexts(element)) + { + if (ContainsLedgerKeyword(text)) + { + return true; + } + } + + return false; + } + + private static bool ContainsLedgerKeyword(string? text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return false; + } + + return text.Contains("收款小账本", StringComparison.OrdinalIgnoreCase) || + text.Contains("微信收款助手", StringComparison.OrdinalIgnoreCase) || + text.Contains("微信支付", StringComparison.OrdinalIgnoreCase) || + text.Contains("收款到账", StringComparison.OrdinalIgnoreCase) || + text.Contains("收款通知", StringComparison.OrdinalIgnoreCase) || + text.Contains("收款记录", StringComparison.OrdinalIgnoreCase) || + text.Contains("收款设置", StringComparison.OrdinalIgnoreCase) || + text.Contains("今日收款", StringComparison.OrdinalIgnoreCase) || + text.Contains("经营收款服务", StringComparison.OrdinalIgnoreCase) || + text.Contains("补充经营信息", StringComparison.OrdinalIgnoreCase); + } + + private static List ExtractAutomationText(AutomationElement root, int max) + { + var results = new List(); + try + { + var walker = TreeWalker.RawViewWalker; + var stack = new Stack(); + stack.Push(root); + while (stack.Count > 0 && results.Count < max) + { + var current = stack.Pop(); + foreach (var text in ExtractElementTexts(current)) + { + if (results.Count >= max) + { + break; + } + + results.Add(text); + } + + AutomationElement? child = null; + try + { + child = walker.GetFirstChild(current); + } + catch + { + child = null; + } + + while (child != null) + { + stack.Push(child); + try + { + child = walker.GetNextSibling(child); + } + catch + { + break; + } + } + } + } + catch + { + } + + return results; + } + + private static IEnumerable ExtractElementTexts(AutomationElement element) + { + var values = new List(); + + try + { + if (!string.IsNullOrWhiteSpace(element.Current.Name)) + { + values.Add(element.Current.Name.Trim()); + } + } + catch { } + + try + { + if (!string.IsNullOrWhiteSpace(element.Current.HelpText)) + { + values.Add(element.Current.HelpText.Trim()); + } + } + catch { } + + try + { + if (!string.IsNullOrWhiteSpace(element.Current.AutomationId)) + { + values.Add(element.Current.AutomationId.Trim()); + } + } + catch { } + + try + { + if (element.TryGetCurrentPattern(ValuePattern.Pattern, out var valuePatternObj)) + { + var value = ((ValuePattern)valuePatternObj).Current.Value; + if (!string.IsNullOrWhiteSpace(value)) + { + values.Add(value.Trim()); + } + } + } + catch { } + + try + { + if (element.TryGetCurrentPattern(TextPattern.Pattern, out var textPatternObj)) + { + var text = ((TextPattern)textPatternObj).DocumentRange.GetText(256); + if (!string.IsNullOrWhiteSpace(text)) + { + values.Add(text.Trim()); + } + } + } + catch { } + + return values.Where(v => !string.IsNullOrWhiteSpace(v)).Distinct(); + } + + private static bool TryParseWechatLedgerLine(string text, out decimal amount, out string remark) + { + amount = 0; + remark = string.Empty; + if (string.IsNullOrWhiteSpace(text)) + { + return false; + } + + var t = text.Replace(" ", string.Empty).Replace("\u00A0", string.Empty); + if (!t.Contains("收款") && + !t.Contains("微信支付") && + !t.Contains("入账") && + !t.Contains("到账") && + !t.Contains("¥") && + !t.Contains("¥") && + !t.Contains("元")) + { + return false; + } + + // 兼容样式: + // - 微信支付收款0.10元 + // - 收款到账0.10元 + // - ¥0.10 / ¥0.10 + // - 收款0.10 + var match = Regex.Match( + t, + @"(?:微信支付)?(?:收款到账|收款通知|收款)\s*(\d+(?:\.\d{1,2})?)\s*元|(?:¥|¥)\s*(\d+(?:\.\d{1,2})?)|收款\s*(\d+(?:\.\d{1,2})?)", + RegexOptions.IgnoreCase); + if (!match.Success) + { + return false; + } + + var amountText = + match.Groups[1].Success ? match.Groups[1].Value : + match.Groups[2].Success ? match.Groups[2].Value : + match.Groups[3].Value; + if (!decimal.TryParse(amountText, out amount) || amount <= 0) + { + return false; + } + + remark = t; + if (remark.Length > 120) + { + remark = remark[..120]; + } + + return true; + } + + private async Task OnWechatPaymentDetectedAsync(decimal amount, string remark, string raw) + { + var evt = new PaymentEvent + { + Channel = "wechat", + Amount = amount, + OrderNo = remark, + TradeNo = string.Empty, + Payer = "wechat-ledger", + Status = "success", + ReceivedAt = DateTimeOffset.Now, + Raw = raw + }; + + var callbackResult = await ForwardEventToServerAsync(evt); + AddPaymentLog(evt, callbackResult); + Log($"Hook 收到收款:{amount:0.00}({remark})"); + } + + private void UpdateWechatStatusUi() + { + if (lblWechatStatusValue is null) + { + return; + } + + var active = string.Equals(_wechatMonitorMode, "hook", StringComparison.Ordinal) || + string.Equals(_wechatMonitorMode, "protocol", StringComparison.Ordinal); + lblWechatStatusValue.Text = active ? "微信: 在线" : "微信: 离线"; + lblWechatStatusValue.ForeColor = active ? Color.Green : Color.Red; + } + + private void StartWechatProtocol() + { + StopWechatProtocol(); + _wechatProtocolSeen.Clear(); + _wechatProtocolLastPendingCount = null; + _wechatProtocolLastHomeType = null; + _wechatProtocolCts = new CancellationTokenSource(); + var token = _wechatProtocolCts.Token; + + _ = Task.Run(async () => + { + while (!token.IsCancellationRequested) + { + try + { + await PollWechatProtocolOnceAsync(token); + } + catch (OperationCanceledException) + { + break; + } + catch (Exception ex) + { + Log($"协议轮询异常:{ex.Message}"); + } + + var delaySeconds = Math.Min(Math.Max(_config.WechatIntervalSeconds, 1), 60); + try + { + await Task.Delay(TimeSpan.FromSeconds(delaySeconds), token); + } + catch (OperationCanceledException) + { + break; + } + } + }, token); + } + + private void StopWechatProtocol() + { + try { _wechatProtocolCts?.Cancel(); } catch { } + try { _wechatProtocolCts?.Dispose(); } catch { } + _wechatProtocolCts = null; + _wechatProtocolLastPendingCount = null; + _wechatProtocolLastHomeType = null; + } + + private async Task PollWechatProtocolOnceAsync(CancellationToken token) + { + token.ThrowIfCancellationRequested(); + var sid = _config.WechatSid?.Trim() ?? string.Empty; + if (string.IsNullOrWhiteSpace(sid)) + { + Log("协议诊断:未填写微信 SID。"); + return; + } + var version = string.IsNullOrWhiteSpace(_config.WechatApiVersion) ? "7.10.1" : _config.WechatApiVersion.Trim(); + + if (!_config.EnableWheelPolling) + { + Log("协议诊断:接口轮询未启用。"); + return; + } + + await SyncPendingOrdersFromServerAsync(token); + + // 先调用 gethomedata,拿到可用时间区间与最新游标 + var homeUrl = BuildWechatSmallbookUrl(sid, version); + var startTime = DateTimeOffset.Now.AddDays(-7).ToUnixTimeSeconds(); + var endTime = DateTimeOffset.Now.ToUnixTimeSeconds(); + string homeBody = string.Empty; + int? homeType = null; + foreach (var typeCandidate in new[] { 0, 1, 2, 39 }) + { + var homeRequest = new + { + start_time = startTime, + end_time = endTime, + type = typeCandidate + }; + var candidateBody = await SendWechatProtocolRequestAsync(homeUrl, homeRequest, token); + if (string.IsNullOrWhiteSpace(candidateBody)) + { + continue; + } + + WechatSmallbookResponse? candidatePayload = null; + try + { + candidatePayload = JsonSerializer.Deserialize(candidateBody, _jsonOptions); + } + catch + { + candidatePayload = null; + } + + if (candidatePayload?.Retcode == 0) + { + homeBody = candidateBody; + homeType = typeCandidate; + if (_wechatProtocolLastHomeType != typeCandidate) + { + Log($"协议诊断(gethomedata):已命中可用 type={typeCandidate}"); + _wechatProtocolLastHomeType = typeCandidate; + } + break; + } + + if (candidatePayload?.Msg?.Contains("type不合法", StringComparison.OrdinalIgnoreCase) == true) + { + continue; + } + + homeBody = candidateBody; + homeType = typeCandidate; + break; + } + + if (!string.IsNullOrWhiteSpace(homeBody)) + { + try + { + var payload = JsonSerializer.Deserialize(homeBody, _jsonOptions); + if (payload?.Retcode != 0) + { + if (payload != null) + { + Log($"协议诊断(gethomedata):retcode={payload.Retcode} errcode={payload.Errcode} msg={payload.Msg}" + (homeType != null ? $" type={homeType}" : string.Empty)); + } + } + else + { + var incomes = payload.Data?.IncomeList; + if (incomes != null) + { + foreach (var income in incomes) + { + token.ThrowIfCancellationRequested(); + UpdateWechatProtocolCursor(income.TransId, income.Timestamp); + var detailRaw = await FetchWechatTransactionDetailAsync(sid, version, income.TransId, income.Timestamp, token); + var id = !string.IsNullOrWhiteSpace(income.TransId) + ? income.TransId! + : (!string.IsNullOrWhiteSpace(income.RollId) ? income.RollId! : $"{income.Timestamp}|{income.Fee}"); + await ProcessWechatProtocolIncomeAsync( + id: id, + transId: income.TransId, + timestamp: income.Timestamp, + feeCent: income.Fee, + raw: string.IsNullOrWhiteSpace(detailRaw) ? homeBody : detailRaw); + } + } + } + } + catch (Exception ex) + { + Log($"协议诊断(gethomedata):JSON 解析失败:{ex.Message}"); + } + } + + // 再用最新游标请求 classifyread + if (!string.IsNullOrWhiteSpace(_wechatProtocolLastTransId) && _wechatProtocolLastCreateTime > 0) + { + var classifyUrl = BuildWechatClassifyReadUrl(sid, version); + var classifyRequest = new + { + v = version, + trans_id = _wechatProtocolLastTransId, + last_bill_id = (string?)null, + count = 10, + page_num = 1, + create_time = _wechatProtocolLastCreateTime, + last_id = "", + last_create_time = 0, + sid = sid + }; + var classifyBody = await SendWechatProtocolRequestAsync(classifyUrl, classifyRequest, token); + if (!string.IsNullOrWhiteSpace(classifyBody)) + { + try + { + var classifyPayload = JsonSerializer.Deserialize(classifyBody, _jsonOptions); + if (classifyPayload?.Retcode == 0 && classifyPayload.Data?.PersonBillList != null) + { + foreach (var bill in classifyPayload.Data.PersonBillList) + { + token.ThrowIfCancellationRequested(); + UpdateWechatProtocolCursor(bill.TransId, bill.CreateTime); + await ProcessWechatProtocolIncomeAsync( + id: bill.TransId ?? $"{bill.CreateTime}|{bill.TotalFee}", + transId: bill.TransId, + timestamp: bill.CreateTime, + feeCent: bill.TotalFee, + raw: classifyBody); + } + } + else if (classifyPayload != null && classifyPayload.Retcode != 0) + { + Log($"协议诊断(classifyread):retcode={classifyPayload.Retcode} errcode={classifyPayload.Errcode} msg={classifyPayload.Msg}"); + } + } + catch (Exception ex) + { + Log($"协议诊断(classifyread):JSON 解析失败:{ex.Message}"); + } + } + } + } + + private async Task SyncPendingOrdersFromServerAsync(CancellationToken token) + { + var serverUrl = NormalizeServerUrl(_config.ServerUrl); + var apiKey = _config.ApiKey?.Trim() ?? string.Empty; + if (string.IsNullOrWhiteSpace(serverUrl) || string.IsNullOrWhiteSpace(apiKey)) + { + return; + } + + var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); + var sign = CreateMd5(timestamp + apiKey); + var pendingUrl = BuildPendingOrdersUrl(serverUrl, timestamp, sign, type: 1); + + var response = await _httpClient.GetAsync(pendingUrl, token); + var body = await response.Content.ReadAsStringAsync(token); + if (!response.IsSuccessStatusCode) + { + Log($"待支付订单同步失败:HTTP {(int)response.StatusCode} {response.StatusCode}"); + return; + } + + PendingOrdersApiResponse? payload = null; + try + { + payload = JsonSerializer.Deserialize(body, _jsonOptions); + } + catch (Exception ex) + { + var preview = body.Length > 200 ? body[..200] + "..." : body; + Log($"待支付订单同步失败:JSON 解析异常 {ex.Message},响应预览={preview}"); + return; + } + + if (payload?.Code != 1 || payload.Data == null) + { + if (payload != null) + { + Log($"待支付订单同步失败:code={payload.Code} msg={payload.Msg}"); + } + return; + } + + _pendingOrders = payload.Data + .Select(x => new PendingOrderRecord + { + OrderId = x.OrderId ?? string.Empty, + PayId = x.PayId ?? string.Empty, + Param = x.Param ?? string.Empty, + PayType = x.PayType, + Price = x.Price, + ReallyPrice = x.ReallyPrice, + TimeOut = x.TimeOut, + State = x.State, + Date = x.Date, + RegisteredAt = DateTimeOffset.Now + }) + .Where(x => !string.IsNullOrWhiteSpace(x.OrderId) && !string.IsNullOrWhiteSpace(x.PayId)) + .ToList(); + SavePendingOrders(); + if (_wechatProtocolLastPendingCount != _pendingOrders.Count) + { + Log($"待支付订单同步成功:共 {_pendingOrders.Count} 条微信待支付订单"); + _wechatProtocolLastPendingCount = _pendingOrders.Count; + } + } + + private static string BuildWechatSmallbookUrl(string sid, string version) + { + var v = string.IsNullOrWhiteSpace(version) ? "7.10.1" : version.Trim(); + return $"https://smallbook.wxpapp.weixin.qq.com/qrappzd/user/gethomedata?sid={Uri.EscapeDataString(sid)}&v={Uri.EscapeDataString(v)}"; + } + + private static string BuildWechatClassifyReadUrl(string sid, string version) + { + var v = string.IsNullOrWhiteSpace(version) ? "7.10.1" : version.Trim(); + return $"https://smallbook.wxpapp.weixin.qq.com/qrappzd/user/classifyread?sid={Uri.EscapeDataString(sid)}&v={Uri.EscapeDataString(v)}"; + } + + private static string BuildWechatTransactionDetailUrl(string sid, string version) + { + var v = string.IsNullOrWhiteSpace(version) ? "7.10.1" : version.Trim(); + return $"https://smallbook.wxpapp.weixin.qq.com/qrappzd/user/transactiondetail?sid={Uri.EscapeDataString(sid)}&v={Uri.EscapeDataString(v)}"; + } + + private async Task SendWechatProtocolRequestAsync(string url, object payload, CancellationToken token) + { + using var request = new HttpRequestMessage(HttpMethod.Post, url); + request.Headers.TryAddWithoutValidation("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36 MicroMessenger/7.0.20.1781(0x6700143B) NetType/WIFI MiniProgramEnv/Windows WindowsWechat/WMPF WindowsWechat(0x63090a13) UnifiedPCWindowsWechat(0xf254186b) XWEB/19481"); + request.Headers.TryAddWithoutValidation("Accept", "application/json, text/plain, */*"); + request.Headers.TryAddWithoutValidation("X-Module-Name", "mmpaysmbpdreceiptassistmp"); + request.Headers.TryAddWithoutValidation("X-Page", "pages/detail/detail"); + request.Headers.TryAddWithoutValidation("X-Track-Id", $"TB{Guid.NewGuid():N}".ToUpperInvariant()); + request.Headers.TryAddWithoutValidation("xweb_xhr", "1"); + request.Headers.TryAddWithoutValidation("X-Appid", "unknown"); + request.Headers.TryAddWithoutValidation("Sec-Fetch-Site", "cross-site"); + request.Headers.TryAddWithoutValidation("Sec-Fetch-Mode", "cors"); + request.Headers.TryAddWithoutValidation("Sec-Fetch-Dest", "empty"); + request.Headers.TryAddWithoutValidation("Referer", "https://servicewechat.com/wx28be8489b7a36aaa/1167/page-frame.html"); + request.Headers.TryAddWithoutValidation("Accept-Language", "zh-CN,zh;q=0.9"); + var json = JsonSerializer.Serialize(payload, _jsonOptions); + request.Content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await _httpClient.SendAsync(request, token); + var body = await response.Content.ReadAsStringAsync(token); + if (!response.IsSuccessStatusCode) + { + Log($"协议诊断:HTTP {(int)response.StatusCode},url={url},响应={body}"); + return string.Empty; + } + + return body; + } + + private async Task FetchWechatTransactionDetailAsync(string sid, string version, string? transId, long createTime, CancellationToken token) + { + if (string.IsNullOrWhiteSpace(transId) || createTime <= 0) + { + return string.Empty; + } + + var url = BuildWechatTransactionDetailUrl(sid, version); + var payload = new + { + v = version, + id = transId, + create_time = createTime, + sid = sid + }; + + var body = await SendWechatProtocolRequestAsync(url, payload, token); + if (string.IsNullOrWhiteSpace(body)) + { + return string.Empty; + } + + try + { + var parsed = JsonSerializer.Deserialize(body, _jsonOptions); + if (parsed?.Retcode != 0) + { + Log($"协议诊断(transactiondetail):retcode={parsed?.Retcode} errcode={parsed?.Errcode} msg={parsed?.Msg}"); + return string.Empty; + } + } + catch (Exception ex) + { + Log($"协议诊断(transactiondetail):JSON 解析失败:{ex.Message}"); + return string.Empty; + } + + return body; + } + + private void UpdateWechatProtocolCursor(string? transId, long createTime) + { + if (!string.IsNullOrWhiteSpace(transId)) + { + _wechatProtocolLastTransId = transId.Trim(); + } + + if (createTime > _wechatProtocolLastCreateTime) + { + _wechatProtocolLastCreateTime = createTime; + } + } + + private async Task ProcessWechatProtocolIncomeAsync(string id, string? transId, long timestamp, decimal feeCent, string raw) + { + if (string.IsNullOrWhiteSpace(id) || !_wechatProtocolSeen.Add(id)) + { + return; + } + + var amount = feeCent > 0 ? feeCent / 100m : 0m; + if (amount <= 0) + { + return; + } + + var receivedAt = timestamp > 0 + ? DateTimeOffset.FromUnixTimeSeconds(timestamp) + : DateTimeOffset.Now; + var remark = string.IsNullOrWhiteSpace(transId) ? id : transId!; + + var evt = new PaymentEvent + { + Channel = "wechat", + Amount = amount, + OrderNo = remark, + TradeNo = transId ?? string.Empty, + Payer = "wechat-protocol", + Status = "success", + ReceivedAt = receivedAt, + Raw = raw + }; + + var callbackResult = await ForwardEventToServerAsync(evt); + AddPaymentLog(evt, callbackResult); + Log($"协议收到收款:{amount:0.00},订单={remark}"); + } + private async void btnToggleService_Click(object? sender, EventArgs e) { if (IsListenerRunning()) @@ -1366,6 +2544,20 @@ namespace Vmianqian var body = await reader.ReadToEndAsync(); Log($"收到本地事件:{body}"); + var registration = TryParsePendingOrderRegistration(body); + if (registration != null) + { + RegisterPendingOrder(registration); + await WriteJsonResponseAsync(context.Response, 200, new + { + ok = true, + registered = true, + orderId = registration.OrderId, + payId = registration.PayId + }); + return; + } + var paymentEvent = JsonSerializer.Deserialize(body, _jsonOptions); if (paymentEvent == null) { @@ -1393,6 +2585,62 @@ namespace Vmianqian } } + private PendingOrderRegistration? TryParsePendingOrderRegistration(string body) + { + try + { + var registration = JsonSerializer.Deserialize(body, _jsonOptions); + if (registration == null || + string.IsNullOrWhiteSpace(registration.OrderId) || + string.IsNullOrWhiteSpace(registration.PayId)) + { + return null; + } + + return registration; + } + catch + { + return null; + } + } + + private void RegisterPendingOrder(PendingOrderRegistration registration) + { + var existing = _pendingOrders.FirstOrDefault(x => string.Equals(x.OrderId, registration.OrderId, StringComparison.OrdinalIgnoreCase)); + if (existing != null) + { + existing.PayId = registration.PayId.Trim(); + existing.Param = registration.Param?.Trim() ?? string.Empty; + existing.Price = registration.Price; + existing.ReallyPrice = registration.ReallyPrice <= 0 ? registration.Price : registration.ReallyPrice; + existing.PayType = registration.PayType; + existing.Date = registration.Date; + existing.TimeOut = registration.TimeOut; + existing.State = registration.State; + existing.RegisteredAt = DateTimeOffset.Now; + } + else + { + _pendingOrders.Add(new PendingOrderRecord + { + OrderId = registration.OrderId.Trim(), + PayId = registration.PayId.Trim(), + Param = registration.Param?.Trim() ?? string.Empty, + Price = registration.Price, + ReallyPrice = registration.ReallyPrice <= 0 ? registration.Price : registration.ReallyPrice, + PayType = registration.PayType, + Date = registration.Date, + TimeOut = registration.TimeOut, + State = registration.State, + RegisteredAt = DateTimeOffset.Now + }); + } + + SavePendingOrders(); + Log($"订单登记成功:orderId={registration.OrderId} payId={registration.PayId} price={registration.Price:0.##}"); + } + private async Task ForwardEventToServerAsync(PaymentEvent paymentEvent) { var serverUrl = NormalizeServerUrl(_config.ServerUrl); @@ -1405,24 +2653,44 @@ namespace Vmianqian }; } - var payload = new ServerCallbackPayload + var matchedOrder = MatchPendingOrder(paymentEvent); + if (matchedOrder == null) { - ApiKey = _config.ApiKey, - Channel = paymentEvent.Channel, - Amount = paymentEvent.Amount, - OrderNo = paymentEvent.OrderNo, - TradeNo = paymentEvent.TradeNo, - Payer = paymentEvent.Payer, - Status = paymentEvent.Status, - ReceivedAt = paymentEvent.ReceivedAt, - Raw = paymentEvent.Raw - }; + return new ServerCallbackResult + { + StatusCode = HttpStatusCode.Conflict, + ResponseBody = "未匹配到待支付订单,已跳过回调。" + }; + } - Log($"转发到服务端:{serverUrl}"); - var response = await _httpClient.PostAsJsonAsync(serverUrl, payload, _jsonOptions); + var matched = matchedOrder; + var type = matched.PayType > 0 + ? matched.PayType.ToString(System.Globalization.CultureInfo.InvariantCulture) + : (string.Equals(paymentEvent.Channel, "wechat", StringComparison.OrdinalIgnoreCase) ? "1" : "2"); + var payId = matched.PayId; + var param = matched.Param ?? string.Empty; + var priceValue = matched.Price > 0 ? matched.Price : paymentEvent.Amount; + var reallyPriceValue = matched.ReallyPrice > 0 ? matched.ReallyPrice : paymentEvent.Amount; + var timestamp = DateTimeOffset.Now.ToUnixTimeSeconds().ToString(); + var sign = CreateMd5(matched.OrderId + paymentEvent.TradeNo + timestamp + (_config.ApiKey ?? string.Empty)); + + var callbackUrl = BuildAppPushOrderUrl( + serverUrl, + matched.OrderId, + paymentEvent.TradeNo, + timestamp, + sign); + + Log($"转发到服务端:{callbackUrl}"); + var response = await _httpClient.GetAsync(callbackUrl); var responseBody = await response.Content.ReadAsStringAsync(); Log($"服务端响应:HTTP {(int)response.StatusCode} {response.StatusCode},内容:{responseBody}"); + if (response.IsSuccessStatusCode && (responseBody.Contains("\"code\":1", StringComparison.OrdinalIgnoreCase) || responseBody.Contains("\"msg\":\"成功\"", StringComparison.OrdinalIgnoreCase) || responseBody.Contains("success", StringComparison.OrdinalIgnoreCase))) + { + MarkPendingOrderCompleted(matched.OrderId, paymentEvent.TradeNo); + } + return new ServerCallbackResult { StatusCode = response.StatusCode, @@ -1430,6 +2698,94 @@ namespace Vmianqian }; } + private PendingOrderRecord? MatchPendingOrder(PaymentEvent paymentEvent) + { + var expectedType = string.Equals(paymentEvent.Channel, "wechat", StringComparison.OrdinalIgnoreCase) ? 1 : 2; + var now = paymentEvent.ReceivedAt == default ? DateTimeOffset.Now : paymentEvent.ReceivedAt; + + var candidate = _pendingOrders + .Where(x => x.State == 0 && x.PayType == expectedType) + .Where(x => Math.Abs(x.Price - paymentEvent.Amount) < 0.0001m || Math.Abs(x.ReallyPrice - paymentEvent.Amount) < 0.0001m) + .OrderByDescending(x => x.Date) + .ThenByDescending(x => x.RegisteredAt) + .FirstOrDefault(x => + { + if (x.Date <= 0) + { + return true; + } + + var createdAt = DateTimeOffset.FromUnixTimeSeconds(x.Date); + var timeoutMinutes = x.TimeOut > 0 ? x.TimeOut : 30; + return now >= createdAt.AddMinutes(-1) && now <= createdAt.AddMinutes(timeoutMinutes + 2); + }); + + if (candidate != null) + { + Log($"匹配待支付订单成功:orderId={candidate.OrderId} payId={candidate.PayId} param={candidate.Param}"); + } + else + { + Log($"未匹配到待支付订单:channel={paymentEvent.Channel} amount={paymentEvent.Amount:0.##} tradeNo={paymentEvent.TradeNo}"); + } + + return candidate; + } + + private void MarkPendingOrderCompleted(string orderId, string? tradeNo) + { + var order = _pendingOrders.FirstOrDefault(x => string.Equals(x.OrderId, orderId, StringComparison.OrdinalIgnoreCase)); + if (order == null) + { + return; + } + + order.State = 1; + order.TradeNo = tradeNo ?? string.Empty; + order.CompletedAt = DateTimeOffset.Now; + SavePendingOrders(); + Log($"待支付订单已标记完成:orderId={orderId}"); + } + + private static string BuildLegacyNotifyUrl(string serverUrl, string payId, string param, string type, string price, string reallyPrice, string sign) + { + var query = new List + { + $"payId={Uri.EscapeDataString(payId)}", + $"param={Uri.EscapeDataString(param)}", + $"type={Uri.EscapeDataString(type)}", + $"price={Uri.EscapeDataString(price)}", + $"reallyPrice={Uri.EscapeDataString(reallyPrice)}", + $"sign={Uri.EscapeDataString(sign)}" + }; + + var separator = serverUrl.Contains('?') ? "&" : "?"; + return serverUrl + separator + string.Join("&", query); + } + + private static string BuildAppPushOrderUrl(string serverUrl, string orderId, string tradeNo, string timestamp, string sign) + { + if (!Uri.TryCreate(serverUrl, UriKind.Absolute, out var baseUri)) + { + throw new InvalidOperationException("服务端地址格式无效。"); + } + + var pushUri = new Uri(baseUri, "/appPushOrder"); + var query = new List + { + $"orderId={Uri.EscapeDataString(orderId)}", + $"tradeNo={Uri.EscapeDataString(tradeNo ?? string.Empty)}", + $"t={Uri.EscapeDataString(timestamp)}", + $"sign={Uri.EscapeDataString(sign)}" + }; + return pushUri + "?" + string.Join("&", query); + } + + private static string BuildPendingOrdersUrl(string serverUrl, string timestamp, string sign, int type) + { + return $"{serverUrl.TrimEnd('/')}/getPendingOrders?t={Uri.EscapeDataString(timestamp)}&sign={Uri.EscapeDataString(sign)}&type={type}"; + } + private void AddPaymentLog(PaymentEvent paymentEvent, ServerCallbackResult callbackResult) { if (InvokeRequired) @@ -1681,7 +3037,7 @@ namespace Vmianqian lblTopNotice.Text = isRunning ? $"监听中:{BuildLocalListenUrl(_config)}" - : "监听未启动"; + : string.Empty; if (btnToggleService != null) { @@ -1704,6 +3060,197 @@ namespace Vmianqian }; } + private static bool TryExtractSidFromClipboard(out string sid, out string version) + { + sid = string.Empty; + version = string.Empty; + try + { + if (!Clipboard.ContainsText()) + { + return false; + } + + var text = Clipboard.GetText(); + return TryExtractSidFromText(text, out sid, out version); + } + catch + { + return false; + } + } + + private static (string Sid, string Version, string Source) TryExtractSidFromWechatLocalFiles() + { + foreach (var root in GetWechatDataRoots()) + { + if (!Directory.Exists(root)) + { + continue; + } + + var scanned = 0; + foreach (var file in EnumerateWechatCandidateFiles(root)) + { + if (scanned >= 2500) + { + break; + } + + scanned++; + try + { + var info = new FileInfo(file); + if (!info.Exists || info.Length <= 0 || info.Length > 16 * 1024 * 1024) + { + continue; + } + + string? text = null; + try + { + text = File.ReadAllText(file); + } + catch + { + text = null; + } + + if (!string.IsNullOrWhiteSpace(text) && TryExtractSidFromText(text, out var sid, out var version)) + { + return (sid, version, file); + } + + if (TryExtractSidFromBinary(file, out sid, out version)) + { + return (sid, version, file); + } + } + catch + { + } + } + } + + return (string.Empty, string.Empty, string.Empty); + } + + private static IEnumerable GetWechatDataRoots() + { + var appData = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData); + var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); + yield return Path.Combine(appData, "Tencent", "WeChat"); + yield return Path.Combine(appData, "Tencent", "Weixin"); + yield return Path.Combine(appData, "Tencent", "WeChatAppEx"); + yield return Path.Combine(localAppData, "Tencent", "WeChat"); + yield return Path.Combine(localAppData, "Tencent", "Weixin"); + yield return Path.Combine(localAppData, "Tencent", "WeChatAppEx"); + } + + private static IEnumerable EnumerateWechatCandidateFiles(string root) + { + var allowed = new HashSet(StringComparer.OrdinalIgnoreCase) + { + ".log", ".txt", ".json", ".js", ".html", ".cache", ".db", ".dat" + }; + + IEnumerator? it = null; + try + { + it = Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories).GetEnumerator(); + } + catch + { + yield break; + } + + using (it) + { + while (true) + { + string? file = null; + try + { + if (!it.MoveNext()) + { + yield break; + } + + file = it.Current; + } + catch + { + continue; + } + + if (string.IsNullOrWhiteSpace(file)) + { + continue; + } + + var ext = Path.GetExtension(file); + if (!allowed.Contains(ext)) + { + continue; + } + + yield return file; + } + } + } + + private static bool TryExtractSidFromText(string? text, out string sid, out string version) + { + sid = string.Empty; + version = string.Empty; + if (string.IsNullOrWhiteSpace(text)) + { + return false; + } + + var match = Regex.Match( + text, + @"(?:\?|&|""|\s)sid=([A-Za-z0-9\-_]{16,})", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + if (!match.Success) + { + return false; + } + + sid = match.Groups[1].Value.Trim(); + var vMatch = Regex.Match( + text, + @"(?:\?|&|""|\s)v=([0-9]+(?:\.[0-9]+){1,3})", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant); + if (vMatch.Success) + { + version = vMatch.Groups[1].Value.Trim(); + } + return !string.IsNullOrWhiteSpace(sid); + } + + private static bool TryExtractSidFromBinary(string file, out string sid, out string version) + { + sid = string.Empty; + version = string.Empty; + try + { + var bytes = File.ReadAllBytes(file); + if (bytes.Length == 0) + { + return false; + } + + // 以拉丁编码保留字节映射,便于从二进制中检索 ASCII URL/sid 片段 + var text = Encoding.Latin1.GetString(bytes); + return TryExtractSidFromText(text, out sid, out version); + } + catch + { + return false; + } + } + private static void ValidateConfig(ClientConfig config) { if (config.ListenPort < 1 || config.ListenPort > 65535) @@ -1845,6 +3392,7 @@ namespace Vmianqian public string EmailAuthCode { get; set; } = string.Empty; public string WechatPath { get; set; } = string.Empty; public string WechatSid { get; set; } = string.Empty; + public string WechatApiVersion { get; set; } = "7.10.1"; public string AlipayPath { get; set; } = string.Empty; public string AlipayAppId { get; set; } = string.Empty; public string AlipayUserId { get; set; } = string.Empty; @@ -1869,6 +3417,42 @@ namespace Vmianqian public string? Raw { get; set; } } + public sealed class PendingOrderRegistration + { + public string OrderId { get; set; } = string.Empty; + public string PayId { get; set; } = string.Empty; + public string? Param { get; set; } + public int PayType { get; set; } + public decimal Price { get; set; } + public decimal ReallyPrice { get; set; } + public int TimeOut { get; set; } + public int State { get; set; } + public long Date { get; set; } + } + + public sealed class PendingOrderRecord + { + public string OrderId { get; set; } = string.Empty; + public string PayId { get; set; } = string.Empty; + public string Param { get; set; } = string.Empty; + public int PayType { get; set; } + public decimal Price { get; set; } + public decimal ReallyPrice { get; set; } + public int TimeOut { get; set; } + public int State { get; set; } + public long Date { get; set; } + public DateTimeOffset RegisteredAt { get; set; } + public DateTimeOffset? CompletedAt { get; set; } + public string TradeNo { get; set; } = string.Empty; + } + + public sealed class PendingOrdersApiResponse + { + public int Code { get; set; } + public string Msg { get; set; } = string.Empty; + public List? Data { get; set; } + } + public sealed class ServerCallbackPayload { public string ApiKey { get; set; } = string.Empty; @@ -1921,4 +3505,89 @@ namespace Vmianqian [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] public int? JkState { get; set; } } + + public sealed class WechatSmallbookResponse + { + [JsonPropertyName("retcode")] + public int Retcode { get; set; } + + [JsonPropertyName("errcode")] + public int Errcode { get; set; } + + [JsonPropertyName("msg")] + public string Msg { get; set; } = string.Empty; + + [JsonPropertyName("data")] + public WechatSmallbookData? Data { get; set; } + } + + public sealed class WechatApiBaseResponse + { + [JsonPropertyName("retcode")] + public int Retcode { get; set; } + + [JsonPropertyName("errcode")] + public int Errcode { get; set; } + + [JsonPropertyName("msg")] + public string Msg { get; set; } = string.Empty; + } + + public sealed class WechatSmallbookData + { + [JsonPropertyName("income_list")] + public List IncomeList { get; set; } = new(); + } + + public sealed class WechatSmallbookIncome + { + [JsonPropertyName("fee")] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public decimal Fee { get; set; } + + [JsonPropertyName("timestamp")] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public long Timestamp { get; set; } + + [JsonPropertyName("trans_id")] + public string? TransId { get; set; } + + [JsonPropertyName("roll_id")] + public string? RollId { get; set; } + } + + public sealed class WechatClassifyReadResponse + { + [JsonPropertyName("retcode")] + public int Retcode { get; set; } + + [JsonPropertyName("errcode")] + public int Errcode { get; set; } + + [JsonPropertyName("msg")] + public string Msg { get; set; } = string.Empty; + + [JsonPropertyName("data")] + public WechatClassifyReadData? Data { get; set; } + } + + public sealed class WechatClassifyReadData + { + [JsonPropertyName("person_bill_list")] + public List PersonBillList { get; set; } = new(); + } + + public sealed class WechatClassifyReadBill + { + [JsonPropertyName("trans_id")] + public string? TransId { get; set; } + + [JsonPropertyName("create_time")] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public long CreateTime { get; set; } + + [JsonPropertyName("total_fee")] + [JsonNumberHandling(JsonNumberHandling.AllowReadingFromString)] + public decimal TotalFee { get; set; } + } } diff --git a/Vmianqian.csproj b/Vmianqian.csproj index 0bb12e1..4f9251e 100644 --- a/Vmianqian.csproj +++ b/Vmianqian.csproj @@ -14,6 +14,15 @@ + + + C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App\10.0.5\UIAutomationClient.dll + + + C:\Program Files\dotnet\shared\Microsoft.WindowsDesktop.App\10.0.5\UIAutomationTypes.dll + + + diff --git a/bin/Debug/net10.0-windows/Vmianqian.dll b/bin/Debug/net10.0-windows/Vmianqian.dll index e9e00fd..f024765 100644 Binary files a/bin/Debug/net10.0-windows/Vmianqian.dll and b/bin/Debug/net10.0-windows/Vmianqian.dll differ diff --git a/bin/Debug/net10.0-windows/Vmianqian.exe b/bin/Debug/net10.0-windows/Vmianqian.exe index 24cdd3c..d623a2a 100644 Binary files a/bin/Debug/net10.0-windows/Vmianqian.exe and b/bin/Debug/net10.0-windows/Vmianqian.exe differ diff --git a/bin/Debug/net10.0-windows/Vmianqian.pdb b/bin/Debug/net10.0-windows/Vmianqian.pdb index fa8c3a0..3c7f8b9 100644 Binary files a/bin/Debug/net10.0-windows/Vmianqian.pdb and b/bin/Debug/net10.0-windows/Vmianqian.pdb differ diff --git a/bin/Debug/net10.0-windows/appsettings.client.json b/bin/Debug/net10.0-windows/appsettings.client.json index 3bfebdf..218aa1e 100644 --- a/bin/Debug/net10.0-windows/appsettings.client.json +++ b/bin/Debug/net10.0-windows/appsettings.client.json @@ -6,8 +6,9 @@ "SmtpPort": 465, "NotifyEmail": "1066960883@qq.com", "EmailAuthCode": "TPPMKSMvCadyzu3m", - "WechatPath": "", - "WechatSid": "", + "WechatPath": "D:\\Softwares\\Tencent\\Weixin\\Weixin.exe", + "WechatSid": "AAHRxMH-4RkRMiXVf7NSJGhBKBwThd_tfDct27hXjkv0Ag", + "WechatApiVersion": "7.10.1", "AlipayPath": "", "AlipayAppId": "", "AlipayUserId": "", @@ -15,7 +16,7 @@ "WechatIntervalSeconds": 5, "AlipayIntervalSeconds": 5, "EnableWheelPolling": true, - "EnableHeartbeat": true, + "EnableHeartbeat": false, "HeartbeatIntervalSeconds": 30, "ListenPath": "/notify/" } \ No newline at end of file diff --git a/bin/Debug/net10.0-windows/pending-orders.client.json b/bin/Debug/net10.0-windows/pending-orders.client.json new file mode 100644 index 0000000..ad47dbb --- /dev/null +++ b/bin/Debug/net10.0-windows/pending-orders.client.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/bin_temp/Vmianqian.dll b/bin_temp/Vmianqian.dll index e9e00fd..aa2cb38 100644 Binary files a/bin_temp/Vmianqian.dll and b/bin_temp/Vmianqian.dll differ diff --git a/bin_temp/Vmianqian.exe b/bin_temp/Vmianqian.exe index 24cdd3c..d623a2a 100644 Binary files a/bin_temp/Vmianqian.exe and b/bin_temp/Vmianqian.exe differ diff --git a/bin_temp/Vmianqian.pdb b/bin_temp/Vmianqian.pdb index fa8c3a0..5100fa7 100644 Binary files a/bin_temp/Vmianqian.pdb and b/bin_temp/Vmianqian.pdb differ diff --git a/obj/Debug/net10.0-windows/Vmianqian.AssemblyInfo.cs b/obj/Debug/net10.0-windows/Vmianqian.AssemblyInfo.cs index 7c6fce0..2697f90 100644 --- a/obj/Debug/net10.0-windows/Vmianqian.AssemblyInfo.cs +++ b/obj/Debug/net10.0-windows/Vmianqian.AssemblyInfo.cs @@ -1,10 +1,9 @@ //------------------------------------------------------------------------------ // -// 此代码由工具生成。 -// 运行时版本:4.0.30319.42000 +// This code was generated by a tool. // -// 对此文件的更改可能会导致不正确的行为,并且如果 -// 重新生成代码,这些更改将会丢失。 +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. // //------------------------------------------------------------------------------ @@ -14,7 +13,7 @@ using System.Reflection; [assembly: System.Reflection.AssemblyCompanyAttribute("Vmianqian")] [assembly: System.Reflection.AssemblyConfigurationAttribute("Debug")] [assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0.0")] -[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+0de0dc29e29fb873a83396b40d1d356b96fd403f")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+82b3bc2cf0c49cfa7245d5fdd65ef87ea19892fc")] [assembly: System.Reflection.AssemblyProductAttribute("Vmianqian")] [assembly: System.Reflection.AssemblyTitleAttribute("Vmianqian")] [assembly: System.Reflection.AssemblyVersionAttribute("1.0.0.0")] diff --git a/obj/Debug/net10.0-windows/Vmianqian.AssemblyInfoInputs.cache b/obj/Debug/net10.0-windows/Vmianqian.AssemblyInfoInputs.cache index 5d730bd..27bcdf3 100644 --- a/obj/Debug/net10.0-windows/Vmianqian.AssemblyInfoInputs.cache +++ b/obj/Debug/net10.0-windows/Vmianqian.AssemblyInfoInputs.cache @@ -1 +1 @@ -2470aa52bf7c9d2f34f885ab74c7c3690d381bd0289dbaafe7d1e260701d7524 +ebd5591aa83e3ce6dc8b4661368f70755a868c935951ae1bef6bc75ef5c3797f diff --git a/obj/Debug/net10.0-windows/Vmianqian.csproj.AssemblyReference.cache b/obj/Debug/net10.0-windows/Vmianqian.csproj.AssemblyReference.cache index 35108ef..695e0e9 100644 Binary files a/obj/Debug/net10.0-windows/Vmianqian.csproj.AssemblyReference.cache and b/obj/Debug/net10.0-windows/Vmianqian.csproj.AssemblyReference.cache differ diff --git a/obj/Debug/net10.0-windows/Vmianqian.csproj.CoreCompileInputs.cache b/obj/Debug/net10.0-windows/Vmianqian.csproj.CoreCompileInputs.cache index 176a9bf..5c232f3 100644 --- a/obj/Debug/net10.0-windows/Vmianqian.csproj.CoreCompileInputs.cache +++ b/obj/Debug/net10.0-windows/Vmianqian.csproj.CoreCompileInputs.cache @@ -1 +1 @@ -1444586d3f97cf78236527678a31115203cb3c8ebe795b8d0d33e2075e906f3f +7239ca1570e58a84bff4ce01bdd3920794f5c0a911cc1d186c71d7544994c98a diff --git a/obj/Debug/net10.0-windows/Vmianqian.csproj.FileListAbsolute.txt b/obj/Debug/net10.0-windows/Vmianqian.csproj.FileListAbsolute.txt index 89c6a41..fd3c390 100644 --- a/obj/Debug/net10.0-windows/Vmianqian.csproj.FileListAbsolute.txt +++ b/obj/Debug/net10.0-windows/Vmianqian.csproj.FileListAbsolute.txt @@ -32,7 +32,6 @@ E:\Demos\DemoOwns\C\VmianqianC\obj\Debug\net10.0-windows\Vmianqian.csproj.CoreCo E:\Demos\DemoOwns\C\VmianqianC\obj\Debug\net10.0-windows\Vmianqian.dll E:\Demos\DemoOwns\C\VmianqianC\obj\Debug\net10.0-windows\refint\Vmianqian.dll E:\Demos\DemoOwns\C\VmianqianC\obj\Debug\net10.0-windows\Vmianqian.pdb -E:\Demos\DemoOwns\C\VmianqianC\bin\Debug\net10.0-windows\Vmianqian.exe E:\Demos\DemoOwns\C\VmianqianC\bin\Debug\net10.0-windows\Vmianqian.deps.json E:\Demos\DemoOwns\C\VmianqianC\bin\Debug\net10.0-windows\Vmianqian.runtimeconfig.json E:\Demos\DemoOwns\C\VmianqianC\bin\Debug\net10.0-windows\Vmianqian.dll @@ -53,3 +52,4 @@ E:\Demos\DemoOwns\C\VmianqianC\bin_temp\MimeKit.dll E:\Demos\DemoOwns\C\VmianqianC\bin\Debug\net10.0-windows\BouncyCastle.Cryptography.dll E:\Demos\DemoOwns\C\VmianqianC\bin\Debug\net10.0-windows\MailKit.dll E:\Demos\DemoOwns\C\VmianqianC\bin\Debug\net10.0-windows\MimeKit.dll +E:\Demos\DemoOwns\C\VmianqianC\bin\Debug\net10.0-windows\Vmianqian.exe diff --git a/obj/Debug/net10.0-windows/Vmianqian.dll b/obj/Debug/net10.0-windows/Vmianqian.dll index e9e00fd..f024765 100644 Binary files a/obj/Debug/net10.0-windows/Vmianqian.dll and b/obj/Debug/net10.0-windows/Vmianqian.dll differ diff --git a/obj/Debug/net10.0-windows/Vmianqian.pdb b/obj/Debug/net10.0-windows/Vmianqian.pdb index fa8c3a0..3c7f8b9 100644 Binary files a/obj/Debug/net10.0-windows/Vmianqian.pdb and b/obj/Debug/net10.0-windows/Vmianqian.pdb differ diff --git a/obj/Debug/net10.0-windows/apphost.exe b/obj/Debug/net10.0-windows/apphost.exe index 24cdd3c..d623a2a 100644 Binary files a/obj/Debug/net10.0-windows/apphost.exe and b/obj/Debug/net10.0-windows/apphost.exe differ diff --git a/obj/Debug/net10.0-windows/ref/Vmianqian.dll b/obj/Debug/net10.0-windows/ref/Vmianqian.dll index eafc4b5..a94f0f6 100644 Binary files a/obj/Debug/net10.0-windows/ref/Vmianqian.dll and b/obj/Debug/net10.0-windows/ref/Vmianqian.dll differ diff --git a/obj/Debug/net10.0-windows/refint/Vmianqian.dll b/obj/Debug/net10.0-windows/refint/Vmianqian.dll index eafc4b5..a94f0f6 100644 Binary files a/obj/Debug/net10.0-windows/refint/Vmianqian.dll and b/obj/Debug/net10.0-windows/refint/Vmianqian.dll differ diff --git a/vmq-customization-pack/Index.php b/vmq-customization-pack/Index.php new file mode 100644 index 0000000..34e4e13 --- /dev/null +++ b/vmq-customization-pack/Index.php @@ -0,0 +1,735 @@ +getCurl($url); + + //return 'by:vone'; + } + + public function getReturn($code = 1, $msg = "成功", $data = null) + { + return array("code" => $code, "msg" => $msg, "data" => $data); + } + + //后台用户登录 + public function login() + { + $user = input("user"); + $pass = input("pass"); + + $_user = Db::name("setting")->where("vkey", "user")->find(); + if ($user != $_user["vvalue"]) { + return json($this->getReturn(-1, "账号或密码错误")); + } + + $_pass = Db::name("setting")->where("vkey", "pass")->find(); + if ($pass != $_pass["vvalue"]) { + return json($this->getReturn(-1, "账号或密码错误")); + } + + Session::set("admin", 1); + + return json($this->getReturn()); + } + + + //后台菜单 + public function getMenu() + { + if (!Session::has("admin")) { + return json($this->getReturn(-1, "没有登录")); + } + + + $menu = array( + array( + "name" => "系统设置", + "type" => "url", + "url" => "admin/setting.html?t=" . time(), + ), + array( + "name" => "监控端设置", + "type" => "url", + "url" => "admin/jk.html?t=" . time(), + ), + array( + "name" => "微信二维码", + "type" => "menu", + "node" => array( + array( + "name" => "添加", + "type" => "url", + "url" => "admin/addwxqrcode.html?t=" . time(), + ), + array( + "name" => "管理", + "type" => "url", + "url" => "admin/wxqrcodelist.html?t=" . time(), + ) + ), + ), array( + "name" => "支付宝二维码", + "type" => "menu", + "node" => array( + array( + "name" => "添加", + "type" => "url", + "url" => "admin/addzfbqrcode.html?t=" . time(), + ), + array( + "name" => "管理", + "type" => "url", + "url" => "admin/zfbqrcodelist.html?t=" . time(), + ) + ), + ), array( + "name" => "订单列表", + "type" => "url", + "url" => "admin/orderlist.html?t=" . time(), + ), array( + "name" => "Api说明", + "type" => "url", + "url" => "api.html?t=" . time(), + ) + ); + + return json($menu); + + } + + //创建订单 + public function createOrder() + { + $this->closeEndOrder(); + + $payId = input("payId"); + if (!$payId || $payId == "") { + return json($this->getReturn(-1, "请传入商户订单号")); + } + $type = input("type"); + if (!$type || $type == "") { + return json($this->getReturn(-1, "请传入支付方式=>1|微信 2|支付宝")); + } + if ($type != 1 && $type != 2) { + return json($this->getReturn(-1, "支付方式错误=>1|微信 2|支付宝")); + } + + $price = input("price"); + if (!$price || $price == "") { + return json($this->getReturn(-1, "请传入订单金额")); + } + if ($price <= 0) { + return json($this->getReturn(-1, "订单金额必须大于0")); + } + + $sign = input("sign"); + if (!$sign || $sign == "") { + return json($this->getReturn(-1, "请传入签名")); + } + + $isHtml = input("isHtml"); + if (!$isHtml || $isHtml == "") { + $isHtml = 0; + } + $param = input("param"); + if (!$param) { + $param = ""; + } + + $res = Db::name("setting")->where("vkey", "key")->find(); + $key = $res['vvalue']; + + if (input("notifyUrl")) { + $notify_url = input("notifyUrl"); + } else { + $res = Db::name("setting")->where("vkey", "notifyUrl")->find(); + $notify_url = $res['vvalue']; + } + + if (input("returnUrl")) { + $return_url = input("returnUrl"); + } else { + $res = Db::name("setting")->where("vkey", "returnUrl")->find(); + $return_url = $res['vvalue']; + } + + + $_sign = md5($payId . $param . $type . $price . $key); + if ($sign != $_sign) { + return json($this->getReturn(-1, "签名错误")); + } + + $jkstate = Db::name("setting")->where("vkey", "jkstate")->find(); + $jkstate = $jkstate['vvalue']; + if ($jkstate!="1"){ + return json($this->getReturn(-1, "监控端状态异常,请检查")); + + } + + + + $reallyPrice = bcmul($price ,100); + + $payQf = Db::name("setting")->where("vkey", "payQf")->find(); + $payQf = $payQf['vvalue']; + + + $orderId = date("YmdHms") . rand(1, 9) . rand(1, 9) . rand(1, 9) . rand(1, 9); + + $ok = false; + for ($i = 0; $i < 10; $i++) { + $tmpPrice = $reallyPrice . "-" . $type; + + $row = Db::execute("INSERT IGNORE INTO tmp_price (price,oid) VALUES ('" . $tmpPrice . "','".$orderId."')"); + if ($row) { + $ok = true; + break; + } + if ($payQf == 1) { + $reallyPrice++; + } else if ($payQf == 2) { + $reallyPrice--; + } + } + + if (!$ok) { + return json($this->getReturn(-1, "订单超出负荷,请稍后重试")); + } + //echo $reallyPrice; + + $reallyPrice = bcdiv($reallyPrice, 100,2); + + if ($type == 1) { + $payUrl = Db::name("setting")->where("vkey", "wxpay")->find(); + $payUrl = $payUrl['vvalue']; + + } else if ($type == 2) { + $payUrl = Db::name("setting")->where("vkey", "zfbpay")->find(); + $payUrl = $payUrl['vvalue']; + } + + if ($payUrl == "") { + return json($this->getReturn(-1, "请您先进入后台配置程序")); + } + $isAuto = 1; + $_payUrl = Db::name("pay_qrcode") + ->where("price", $reallyPrice) + ->where("type", $type) + ->find(); + if ($_payUrl) { + $payUrl = $_payUrl['pay_url']; + $isAuto = 0; + } + + + $res = Db::name("pay_order")->where("pay_id", $payId)->find(); + if ($res) { + return json($this->getReturn(-1, "商户订单号已存在")); + } + + + + + $createDate = time(); + $data = array( + "close_date" => 0, + "create_date" => $createDate, + "is_auto" => $isAuto, + "notify_url" => $notify_url, + "order_id" => $orderId, + "param" => $param, + "pay_date" => 0, + "pay_id" => $payId, + "pay_url" => $payUrl, + "price" => $price, + "really_price" => $reallyPrice, + "return_url" => $return_url, + "state" => 0, + "type" => $type + + ); + + + Db::name("pay_order")->insert($data); + + + //return ""; + + if ($isHtml == 1) { + + echo ""; + + } else { + $time = Db::name("setting")->where("vkey", "close")->find(); + $data = array( + "payId" => $payId, + "orderId" => $orderId, + "payType" => $type, + "price" => $price, + "reallyPrice" => $reallyPrice, + "payUrl" => $payUrl, + "isAuto" => $isAuto, + "state" => 0, + "timeOut" => $time['vvalue'], + "date" => $createDate + ); + return json($this->getReturn(1, "成功", $data)); + + } + + + } + //获取订单信息 + public function getOrder() + { + + $res = Db::name("pay_order")->where("order_id", input("orderId"))->find(); + if ($res){ + $time = Db::name("setting")->where("vkey", "close")->find(); + + $data = array( + "payId" => $res['pay_id'], + "orderId" => $res['order_id'], + "payType" => $res['type'], + "price" => $res['price'], + "reallyPrice" => $res['really_price'], + "payUrl" => $res['pay_url'], + "isAuto" => $res['is_auto'], + "state" => $res['state'], + "timeOut" => $time['vvalue'], + "date" => $res['create_date'] + ); + return json($this->getReturn(1, "成功", $data)); + }else{ + return json($this->getReturn(-1, "云端订单编号不存在")); + } + } + //查询订单状态 + public function checkOrder() + { + $res = Db::name("pay_order")->where("order_id", input("orderId"))->find(); + if ($res){ + if ($res['state']==0){ + return json($this->getReturn(-1, "订单未支付")); + } + if ($res['state']==-1){ + return json($this->getReturn(-1, "订单已过期")); + } + + $res2 = Db::name("setting")->where("vkey","key")->find(); + $key = $res2['vvalue']; + + $res['price'] = number_format($res['price'],2,".",""); + $res['really_price'] = number_format($res['really_price'],2,".",""); + + + $p = "payId=".$res['pay_id']."¶m=".$res['param']."&type=".$res['type']."&price=".$res['price']."&reallyPrice=".$res['really_price']; + + $sign = $res['pay_id'].$res['param'].$res['type'].$res['price'].$res['really_price'].$key; + $p = $p . "&sign=".md5($sign); + + $url = $res['return_url']; + + + + if (strpos($url,"?")===false){ + $url = $url."?".$p; + }else{ + $url = $url."&".$p; + } + + return json($this->getReturn(1, "成功", $url)); + }else{ + return json($this->getReturn(-1, "云端订单编号不存在")); + } + + } + + //获取待支付订单列表 + public function getPendingOrders() + { + $key = Db::name("setting")->where("vkey", "key")->find(); + $key = $key ? $key['vvalue'] : ""; + $t = input("t"); + $sign = input("sign"); + $type = input("type"); + + if (!$t || !$sign) { + return json($this->getReturn(-1, "请传入时间戳和签名")); + } + + $_sign = md5($t . $key); + if ($_sign != $sign) { + return json($this->getReturn(-1, "签名校验不通过")); + } + + $query = Db::name("pay_order") + ->where("state", 0) + ->order("create_date desc") + ->limit(50); + + if ($type !== null && $type !== '') { + $query->where("type", intval($type)); + } + + $rows = $query->select(); + $data = array(); + foreach ($rows as $row) { + $data[] = array( + "payId" => $row['pay_id'], + "orderId" => $row['order_id'], + "param" => $row['param'], + "payType" => intval($row['type']), + "price" => floatval($row['price']), + "reallyPrice" => floatval($row['really_price']), + "state" => intval($row['state']), + "timeOut" => intval((Db::name("setting")->where("vkey", "close")->find())['vvalue']), + "date" => intval($row['create_date']) + ); + } + + return json($this->getReturn(1, "成功", $data)); + } + //关闭订单 + public function closeOrder(){ + $res2 = Db::name("setting")->where("vkey","key")->find(); + $key = $res2['vvalue']; + $orderId = input("orderId"); + + $_sign = $orderId.$key; + + if (md5($_sign)!=input("sign")){ + return json($this->getReturn(-1, "签名校验不通过")); + } + + $res = Db::name("pay_order")->where("order_id",$orderId)->find(); + + if ($res){ + if ($res['state']!=0){ + return json($this->getReturn(-1, "订单状态不允许关闭")); + } + Db::name("pay_order")->where("order_id",$orderId)->update(array("state"=>-1,"close_date"=>time())); + Db::name("tmp_price") + ->where("oid",$res['order_id']) + ->delete(); + return json($this->getReturn(1, "成功")); + }else{ + return json($this->getReturn(-1, "云端订单编号不存在")); + + } + + } + //获取监控端状态 + public function getState(){ + $res2 = Db::name("setting")->where("vkey","key")->find(); + $key = $res2['vvalue']; + $t = input("t"); + + $_sign = $t.$key; + + if (md5($_sign)!=input("sign")){ + return json($this->getReturn(-1, "签名校验不通过")); + } + + $res = Db::name("setting")->where("vkey","lastheart")->find(); + $lastheart = $res['vvalue']; + $res = Db::name("setting")->where("vkey","lastpay")->find(); + $lastpay = $res['vvalue']; + $res = Db::name("setting")->where("vkey","jkstate")->find(); + $jkstate = $res['vvalue']; + + return json($this->getReturn(1, "成功",array("lastheart"=>$lastheart,"lastpay"=>$lastpay,"jkstate"=>$jkstate))); + + } + + //App心跳接口 + public function appHeart(){ + $this->closeEndOrder(); + + $res2 = Db::name("setting")->where("vkey","key")->find(); + $key = $res2['vvalue']; + $t = input("t"); + + $_sign = $t.$key; + + if (md5($_sign)!=input("sign")){ + return json($this->getReturn(-1, "签名校验不通过")); + } + +// $jg = time()*1000 - $t; +// if ($jg>50000 || $jg<-50000){ +// return json($this->getReturn(-1, "客户端时间错误")); +// } + + Db::name("setting")->where("vkey","lastheart")->update(array("vvalue"=>time())); + Db::name("setting")->where("vkey","jkstate")->update(array("vvalue"=>1)); + return json($this->getReturn()); + } + //App推送付款数据接口 + public function appPush(){ + $this->closeEndOrder(); + + $res2 = Db::name("setting")->where("vkey","key")->find(); + $key = $res2['vvalue']; + $t = input("t"); + $type = input("type"); + $price = input("price"); + + $_sign = $type.$price.$t.$key; + + if (md5($_sign)!=input("sign")){ + return json($this->getReturn(-1, "签名校验不通过")); + } + +// $jg = time()*1000 - $t; +// if ($jg>50000 || $jg<-50000){ +// return json($this->getReturn(-1, "客户端时间错误")); +// } + + Db::name("setting") + ->where("vkey","lastpay") + ->update( + array( + "vvalue"=>time() + ) + ); + + $res = Db::name("pay_order") + ->where("really_price",$price) + ->where("state",0) + ->where("type",$type) + ->find(); + + + + if ($res){ + + Db::name("tmp_price") + ->where("oid",$res['order_id']) + ->delete(); + + Db::name("pay_order")->where("id",$res['id'])->update(array("state"=>1,"pay_date"=>time(),"close_date"=>time())); + + $url = $res['notify_url']; + + $res2 = Db::name("setting")->where("vkey","key")->find(); + $key = $res2['vvalue']; + + $p = "payId=".$res['pay_id']."¶m=".$res['param']."&type=".$res['type']."&price=".$res['price']."&reallyPrice=".$res['really_price']; + + $sign = $res['pay_id'].$res['param'].$res['type'].$res['price'].$res['really_price'].$key; + $p = $p . "&sign=".md5($sign); + + if (strpos($url,"?")===false){ + $url = $url."?".$p; + }else{ + $url = $url."&".$p; + } + + + $re = $this->getCurl($url); + if ($re=="success"){ + return json($this->getReturn()); + }else{ + Db::name("pay_order")->where("id",$res['id'])->update(array("state"=>2)); + + return json($this->getReturn(-1,"异步通知失败")); + } + + + }else{ + $data = array( + "close_date" => 0, + "create_date" => time(), + "is_auto" => 0, + "notify_url" => "", + "order_id" => "无订单转账", + "param" => "无订单转账", + "pay_date" => 0, + "pay_id" => "无订单转账", + "pay_url" => "", + "price" => $price, + "really_price" => $price, + "return_url" => "", + "state" => 1, + "type" => $type + + ); + + Db::name("pay_order")->insert($data); + return json($this->getReturn()); + + } + + + } + + //App按订单推送付款数据接口 + public function appPushOrder(){ + $this->closeEndOrder(); + + $res2 = Db::name("setting")->where("vkey","key")->find(); + $key = $res2['vvalue']; + $orderId = input("orderId"); + $tradeNo = input("tradeNo"); + $t = input("t"); + + if (!$orderId || !$t) { + return json($this->getReturn(-1, "参数不完整")); + } + + $_sign = $orderId.$tradeNo.$t.$key; + if (md5($_sign)!=input("sign")){ + return json($this->getReturn(-1, "签名校验不通过")); + } + + Db::name("setting") + ->where("vkey","lastpay") + ->update(array( + "vvalue"=>time() + )); + + $res = Db::name("pay_order")->where("order_id",$orderId)->find(); + if (!$res){ + return json($this->getReturn(-1, "云端订单编号不存在")); + } + + if ($res['state']==1){ + return json($this->getReturn(1, "订单已完成")); + } + + if ($res['state']!=0){ + return json($this->getReturn(-1, "订单状态不允许推送")); + } + + Db::name("tmp_price") + ->where("oid",$res['order_id']) + ->delete(); + + Db::name("pay_order")->where("id",$res['id'])->update(array("state"=>1,"pay_date"=>time(),"close_date"=>time())); + + $notifyResult = $this->notifyOrder($res); + if ($notifyResult === "success"){ + return json($this->getReturn(1, "成功")); + }else{ + Db::name("pay_order")->where("id",$res['id'])->update(array("state"=>2)); + return json($this->getReturn(-1,"异步通知失败",$notifyResult)); + } + } + + + //关闭过期订单接口(请用定时器至少1分钟调用一次) + public function closeEndOrder(){ + $res = Db::name("setting")->where("vkey","lastheart")->find(); + $lastheart = $res['vvalue']; + if ((time()-$lastheart)>60){ + Db::name("setting")->where("vkey","jkstate")->update(array("vvalue"=>0)); + } + + + + $time = Db::name("setting")->where("vkey", "close")->find(); + + $closeTime = time()-60*$time['vvalue']; + $close_date = time(); + + $res = Db::name("pay_order") + ->where("create_date <=".$closeTime) + ->where("state",0) + ->update(array("state"=>-1,"close_date"=>$close_date)); + + if ($res){ + $rows = Db::name("pay_order")->where("close_date",$close_date)->select(); + foreach ($rows as $row){ + Db::name("tmp_price") + ->where("oid",$row['order_id']) + ->delete(); + } + + $rows = Db::name("tmp_price")->select(); + foreach ($rows as $row){ + $re = Db::name("pay_order")->where("order_id",$row['oid'])->find(); + if ($re){ + + }else{ + Db::name("tmp_price") + ->where("oid",$row['oid']) + ->delete(); + } + } + + + return json($this->getReturn(1,"成功清理".$res."条订单")); + }else{ + return json($this->getReturn(1,"没有等待清理的订单")); + } + + + + } + + + //发送Http请求 + private function getCurl($url, $post = 0, $cookie = 0, $header = 0, $nobaody = 0) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + $klsf[] = 'Accept:*/*'; + $klsf[] = 'Accept-Language:zh-cn'; + //$klsf[] = 'Content-Type:application/json'; + $klsf[] = 'User-Agent:Mozilla/5.0 (iPhone; CPU iPhone OS 11_2_1 like Mac OS X) AppleWebKit/604.4.7 (KHTML, like Gecko) Mobile/15C153 MicroMessenger/6.6.1 NetType/WIFI Language/zh_CN'; + $klsf[] = 'Referer:'.$url; + curl_setopt($ch, CURLOPT_HTTPHEADER, $klsf); + if ($post) { + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, $post); + } + if ($header) { + curl_setopt($ch, CURLOPT_HEADER, true); + } + if ($cookie) { + curl_setopt($ch, CURLOPT_COOKIE, $cookie); + } + if ($nobaody) { + curl_setopt($ch, CURLOPT_NOBODY, 1); + } + curl_setopt($ch, CURLOPT_TIMEOUT,60); + curl_setopt($ch, CURLOPT_ENCODING, 'gzip'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + $ret = curl_exec($ch); + curl_close($ch); + return $ret; + } + + private function notifyOrder($res) + { + $url = $res['notify_url']; + + $res2 = Db::name("setting")->where("vkey","key")->find(); + $key = $res2['vvalue']; + + $p = "payId=".$res['pay_id']."¶m=".$res['param']."&type=".$res['type']."&price=".$res['price']."&reallyPrice=".$res['really_price']; + + $sign = $res['pay_id'].$res['param'].$res['type'].$res['price'].$res['really_price'].$key; + $p = $p . "&sign=".md5($sign); + + if (strpos($url,"?")===false){ + $url = $url."?".$p; + }else{ + $url = $url."&".$p; + } + + return $this->getCurl($url); + } + +} diff --git a/vmq-customization-pack/README.md b/vmq-customization-pack/README.md new file mode 100644 index 0000000..31390ea --- /dev/null +++ b/vmq-customization-pack/README.md @@ -0,0 +1,178 @@ +# V免签微信协议改造说明 + +这个目录用于给其他用户复用本次 V免签 服务端改造。 + +## 目录说明 + +- `route.php` + V免签路由文件改造后的完整版本。 +- `Index.php` + `application/index/controller/Index.php` 改造后的完整版本。 + +## 本次改造解决了什么 + +原版 V免签 更适合传统监控端通过金额回调 `appPush` 完成订单。 +这次改造的目标是让一个独立的微信协议监听客户端可以: + +1. 从 V免签 服务端拉取当前未支付订单 +2. 在本地监听到微信到账后,按 `orderId` 精确回推指定订单 +3. 继续复用 V免签 原本的异步通知逻辑通知商户站点 + +这样就避免了以下问题: + +- 只按金额匹配,多个同金额订单时容易串单 +- 监听端找不到待支付订单时,错误地把微信交易号直接当作 `payId` 去回调 +- 服务端根域名被误请求,返回首页 HTML 而不是订单处理结果 + +## 服务端新增接口 + +### 1. `getPendingOrders` + +用途: +给客户端拉取当前待支付订单列表。 + +请求方式: +`GET` 或 `POST` + +参数: + +- `t` + 当前时间戳 +- `sign` + 签名,算法:`md5(t + 通讯密钥)` +- `type` + 支付类型,微信传 `1`,支付宝传 `2` + +返回: +返回 `state=0` 的待支付订单列表,包含: + +- `payId` +- `orderId` +- `param` +- `payType` +- `price` +- `reallyPrice` +- `state` +- `timeOut` +- `date` + +### 2. `appPushOrder` + +用途: +客户端本地匹配到具体订单后,通知 V免签 将指定订单置为已支付。 + +请求方式: +`GET` 或 `POST` + +参数: + +- `orderId` + 云端订单号 +- `tradeNo` + 微信侧交易号 +- `t` + 当前时间戳 +- `sign` + 签名,算法:`md5(orderId + tradeNo + t + 通讯密钥)` + +处理流程: + +1. 校验签名 +2. 查询对应 `orderId` +3. 订单状态必须为 `0` +4. 更新订单为已支付 +5. 调用原有商户异步通知 +6. 成功返回 JSON,失败则把订单状态更新为 `2` + +## 需要修改的原项目文件 + +如果你自己的 V免签 项目结构和原版一致,只需要替换: + +- `route/route.php` +- `application/index/controller/Index.php` + +建议替换前先备份原文件。 + +## 部署步骤 + +1. 备份线上原文件 +2. 用本目录中的 `route.php` 覆盖线上 `route/route.php` +3. 用本目录中的 `Index.php` 覆盖线上 `application/index/controller/Index.php` +4. 确认线上缓存已刷新 +5. 重新测试客户端与 V免签 的联动 + +## 验证方法 + +### 验证 1:拉取待支付订单 + +先在 V免签 后台或发卡程序创建一笔新的微信订单,确保订单未过期。 + +然后请求: + +```text +https://你的域名/getPendingOrders?t=当前时间戳&sign=md5(t+通讯密钥)&type=1 +``` + +如果成功,应该返回 JSON,`data` 中能看到待支付微信订单。 + +### 验证 2:按订单推送成功 + +当客户端匹配到订单后,应请求: + +```text +https://你的域名/appPushOrder?orderId=云端订单号&tradeNo=微信交易号&t=当前时间戳&sign=md5(orderId+tradeNo+t+通讯密钥) +``` + +成功时应返回类似: + +```json +{"code":1,"msg":"成功","data":null} +``` + +## 与原版的关键差异 + +### 原版 `appPush` + +原版只按下面三个条件查单: + +- `really_price` +- `state=0` +- `type` + +这种方式在同金额并发订单下容易误匹配。 + +### 改造后的 `appPushOrder` + +改造后由客户端先拉待支付订单,再在本地完成: + +- 金额匹配 +- 时间范围过滤 +- 精确得到 `orderId` + +最后再回推给服务端,服务端按 `orderId` 精确处理,不再依赖“金额即订单”。 + +## 适用场景 + +适用于以下场景: + +- 自己有独立的微信监听客户端 +- 不希望直接改发卡系统创建订单逻辑 +- 想保留 V免签 现有订单表、商户通知和后台逻辑 +- 想把“支付监听”和“订单完成”拆成两段处理 + +## 注意事项 + +1. `getPendingOrders` 只会返回未过期且 `state=0` 的订单 +2. 如果客户端一直拉到 `0` 条订单,先检查订单是否已经过期 +3. 如果客户端监听到了微信收款,但没匹配到订单,不应该再回调根域名 +4. 商户异步通知仍然依赖原表中的 `notify_url` +5. 如果你在服务端做了二次开发,替换前先比对自定义逻辑 + +## 推荐客户端配合逻辑 + +为了避免刷历史收款记录,客户端建议再做两件事: + +1. 启动协议监听时忽略历史账单,只处理启动后的新收款 +2. 正常轮询成功时不要持续打印日志,只在状态变化、检测到收款或异常时打印 + +这样用户体验会更好,也更容易排查问题。 diff --git a/vmq-customization-pack/route.php b/vmq-customization-pack/route.php new file mode 100644 index 0000000..3d659c0 --- /dev/null +++ b/vmq-customization-pack/route.php @@ -0,0 +1,37 @@ + +// +---------------------------------------------------------------------- + +Route::get('think', function () { + return 'hello,ThinkPHP5!'; +}); + +Route::any('login','index/index/login'); +Route::any('getMenu','index/index/getMenu'); +Route::any('enQrcode','admin/index/enQrcode'); +Route::any('createOrder','index/index/createOrder'); + + +Route::any('getOrder','index/index/getOrder'); +Route::any('checkOrder','index/index/checkOrder'); +Route::any('getPendingOrders','index/index/getPendingOrders'); +Route::any('getState','index/index/getState'); + +Route::any('appHeart','index/index/appHeart'); +Route::any('appPush','index/index/appPush'); +Route::any('appPushOrder','index/index/appPushOrder'); + + +Route::any('closeEndOrder','index/index/closeEndOrder'); + + +return [ + +];