Mysql安装

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

问题1

System.ArgumentException
HResult=0x80070057
Message=未找到请求的值“none”。
Source=mscorlib
StackTrace:
在 System.Enum.TryParseEnum(Type enumType, String value, Boolean ignoreCase, EnumResult& parseResult)
在 System.Enum.Parse(Type enumType, String value, Boolean ignoreCase)
在 MySql.Data.MySqlClient.MySqlBaseConnectionStringBuilder.<>c.<.cctor>b__1_2(MySqlBaseConnectionStringBuilder msb, MySqlConnectionStringOption sender, Object value)
在 System.Data.Common.DbConnectionStringBuilder.set_ConnectionString(String value)
在 MySql.Data.MySqlClient.MySqlConnectionStringBuilder…ctor(String connectionString, Boolean isAnalyzed)
在 MySql.Data.MySqlClient.MySqlConnection.set_ConnectionString(String value)
在 WpfApp6.Data.MySqlDetectionRepository.d__4.MoveNext() 在 D:\Download\WpfApp6 (2)\WpfApp6\WpfApp6\Data\MySqlDetectionRepository.cs 中: 第 68 行

版本问题,导致部分配置参数不同,修改App.config中SslMode=Preferred解决

问题2

此异常最初是在此调用堆栈中引发的:
[外部代码]
WpfApp6.Data.MySqlDetectionRepository.SaveAsync(WpfApp6.Models.DetectionResult) (位于 MySqlDetectionRepository.cs 中)

MySql.Data.MySqlClient.MySqlException
HResult=0x80004005
Message=Table ‘xraydetectiondb.detectionresults’ doesn’t exist
Source=MySql.Data
StackTrace:
在 MySql.Data.MySqlClient.MySqlStream.CheckForServerError()
在 MySql.Data.MySqlClient.MySqlStream.d__32.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 MySql.Data.MySqlClient.NativeDriver.d__58.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 MySql.Data.MySqlClient.Driver.d__102.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 MySql.Data.MySqlClient.Driver.d__100.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 MySql.Data.MySqlClient.MySqlDataReader.d__99.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 MySql.Data.MySqlClient.MySqlDataReader.d__99.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 MySql.Data.MySqlClient.MySqlCommand.d__115.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 MySql.Data.MySqlClient.MySqlCommand.d__115.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 MySql.Data.MySqlClient.MySqlCommand.d__115.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 MySql.Data.MySqlClient.MySqlCommand.d__100.MoveNext()
在 System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
在 System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
在 System.Runtime.CompilerServices.ConfiguredTaskAwaitable`1.ConfiguredTaskAwaiter.GetResult()
在 WpfApp6.Data.MySqlDetectionRepository.d__4.MoveNext() 在 D:\Download\WpfApp6 (2)\WpfApp6\WpfApp6\Data\MySqlDetectionRepository.cs 中: 第 88 行

此异常最初是在此调用堆栈中引发的:
[外部代码]
WpfApp6.Data.MySqlDetectionRepository.SaveAsync(WpfApp6.Models.DetectionResult) (位于 MySqlDetectionRepository.cs 中)
在这里插入图片描述
未建表 解决

问题3

在这里插入图片描述
下面数据库建立连接已使用using,会自己释放掉,这里直接删除

  public void Dispose()
  {
    //  throw new NotImplementedException();
  }

项目整体大致流程

用户点"点位扫描"
    ↓
MainShellVM 发 ScanStartedMessage          → 自己收(打日志 + 显示扫描中)
MainShellVM 发 StartScanMessage(TargetX, Y, Exp, ScanId)
    ↓
AxisEcatVM 收到 StartScanMessage
    → 模拟轴运动(MoveToAsync + 轮询)
    → 每200ms 定时器发 BusStatusMessage + ScanProgressMessage 更新UI
    ↓ 到位后
AxisEcatVM 发 AxisPositionReadyMessage(X, Y, ScanId)
    ↓
XrayImageVM 收到 AxisPositionReadyMessage
    → 50ms防抖
    → MockImageProvider.GetImage()
    → 显示在 Image 控件上

MainShellVM ──ScanStartedMessage──▶ MainShellVM (状态栏)
            ──ScanStartedMessage──▶ XrayImageVM (记录扫描ID)
            ──StartScanMessage────▶ AxisEcatVM (开始跑)
            ──StopScanMessage─────▶ AxisEcatVM (停止)
            ──StopScanMessage─────▶ XrayImageVM (清图)
            ──UpdateTargetMessage─▶ AxisEcatVM (中途改坐标)
            
AxisEcatVM ──AxisPositionReady──▶ XrayImageVM (显示图像)
            ──LogMessage─────────▶ MainShellVM (日志)
            ──BusStatusMessage───▶ MainShellVM (状态栏)
            ──ScanProgressMessage▶ MainShellVM (进度条)

问题4

lock时序问题 _currentScanId值变化情况
你的直觉是对的——lock 确实能防止同时读写,但 lock 只是管住同一时刻。
关键是两个 lock 块不是连在一起的,中间有时间差:

// 📦 第1个 lock 块:收到 ScanStartedMessage 时
lock (_debounceLock)
{
    _currentScanId = "AAA";   ← 设成了 AAA
    CancelAndDisposeCts();    ← 取消上次防抖
}
// ← 这里就解锁了!_currentScanId 暴露在外面

//         ⏱️ 中间可能过几秒钟
//         用户又点了一次扫描
//         _currentScanId 被改成了 "BBB"

// 📦 第2个 lock 块:收到 AxisPositionReadyMessage(属于"AAA"那次扫描)
lock (_debounceLock)
{
    if (m.ScanId != _currentScanId)  ← 此时读到的 _currentScanId 已经是 "BBB"return;"AAA" != "BBB",丢掉!
}

lock 只管"同一时刻",不管"先后顺序":

时间线:

主轴线程                            副线程(XrayImageVM 的消息)
──────                               ─────────────────────────
                                        lock { _currentScanId = "AAA" }  ← 第一次扫描
                                        unlock
                                        
用户又点了一次扫描
MainShellVM 发 ScanStartedMessage("BBB")
                                        lock { _currentScanId = "BBB" }  ← 第二次扫描覆盖
                                        unlock

第一次扫描的轴到位了
AxisEcatVM 发 AxisPositionReady("AAA")
                                        lock { 
                                            _currentScanId 是 "BBB"
                                            "AAA" != "BBB" → 丢掉
                                        }
                                        unlock

你的理解其实是对的。_currentScanId 确实就是被两次 lock 块改来改去的,只不过:

第1次 lock 改成 “AAA”
第1次 unlock(此时 “AAA” 暴露在外面)
第2次 lock 改成 “BBB”(覆盖了 “AAA”)
第2次 unlock
第3次 lock 来检查时,读到的是 “BBB”

lock 解决的是"两个人同时改"的问题,不是"一个人改完、另一个人读"的问题。 两次 lock { set } 之间没有锁保护,所以值当然可以变。

对,你的理解完全正确。就是时序问题。
我帮你把整个链条画清楚:
code复制

第一次点"点位扫描"
├── MainShellVM 发 ScanStartedMessage("AAA")    
├── MainShellVM 发 StartScanMessage("AAA")      
│
├── XrayImageVM: _currentScanId = "AAA"     ← 存下
└── AxisEcatVM: _activeScanId = "AAA"       ← 存下
    └── RunScanTask 启动(轴开始走……)
                               
第二次点"点位扫描"  ← 用户又点了
├── MainShellVM 发 ScanStartedMessage("BBB")
├── MainShellVM 发 StartScanMessage("BBB")
│
├── XrayImageVM: _currentScanId = "BBB"     ← 被覆盖了
└── AxisEcatVM: _activeScanId = "BBB"       ← 被覆盖了
    └── RunScanTask 启动(取消旧的,起新的)
        旧的 RunScanTask("AAA") 可能还在跑
        新的 RunScanTask("BBB") 也开始跑

第一次扫描的轴到位了
└── 旧的 RunScanTask("AAA") 完成 MoveToAsync
    └── 发 AxisPositionReadyMessage("AAA")

XrayImageVM 收到 
→ "AAA" != _currentScanId("BBB")  
→ ❌ 丢掉!

第二次扫描的轴到位了
└── 新的 RunScanTask("BBB") 完成 MoveToAsync
    └── 发 AxisPositionReadyMessage("BBB")

XrayImageVM 收到
→ "BBB" == _currentScanId("BBB")
→ ✅ 显示图像!

你刚才的细节我纠正一下
你说"XrayImage 的构造方法再订阅准备显示出图"——不是构造方法。XrayImageVM 在 MainWindow 构造函数里就 new 好了:

public MainWindow()
{
    // ...
    ImageVM = new XrayImageVM();   // ← 这里就注册了所有消息监听
    // ...
}

它的订阅(Register)在构造函数里只执行一次,不是每次扫描都重新注册。所以:

XrayImageVM 只有一份实例,活了整个程序生命周期
↓
构造函数里 Register 了三个消息:
  ScanStartedMessage       → 更新 _currentScanId
  AxisPositionReadyMessage → 过滤后显示图像
  StopScanMessage          → 清空
↓
每次扫描发来的消息,同一个人处理,靠 _currentScanId 区分"这是哪一次的"

你可以理解为:XrayImageVM 里有个人,拿着个牌子(_currentScanId),牌子上的字每点一次扫描就换一次,他只认牌子上写的那个 ScanId 的消息。

问题5

我没有显式的绑定:StartScanCommand=new RelayCommand(()=>StartScan) 只是用了MVVMToolKIT的自动绑定?就是StartScan头上加了[RelayCommand] 为啥编译器知道我的StartScanCommand 对应的是StartScan方法

这个问题问到 MVVM Toolkit 的源生成器核心了。简洁解释:
关键:这不是运行时反射,是编译时代码生成
你写这个:

private void StartScan()
{
    // ...
}

编译器在背后帮你自动生成了这个(你看不到的代码):

// 编译器自动生成的 partial 类(你编译后在 obj 文件夹里能看到)
public partial class MainShellVM
{
    public RelayCommand StartScanCommand 
    {
        get
        {
            if (_startScanCommand == null)
                _startScanCommand = new RelayCommand(StartScan);  // ← 这就是你说的那行
            return _startScanCommand;
        }
    }
    private RelayCommand _startScanCommand;
}

为什么能自动生成? 三个条件缺一不可:
在这里插入图片描述
编译器看到这三样,就知道要在另一个 partial 文件里帮你补一个 StartScanCommand 属性。
命名规则

方法名         → 生成的命令名         → XAML 绑定
StartScan     → StartScanCommand     → {Binding MainVM.StartScanCommand}
StopScan      → StopScanCommand      → {Binding MainVM.StopScanCommand}
EmergencyStop → EmergencyStopCommand → {Binding MainVM.EmergencyStopCommand}

规则就是:方法名去掉首字母小写 + “Command”。
这也是为什么之前的代码里注释掉了手写的:
csharp复制

//public RelayCommand StartScanCommand { get; }  ← 写了会报错,因为源生成器也会生成同名的
//public RelayCommand StopScanCommand { get; }
//public RelayCommand EmergencyStopCommand { get; }

如果手动写和源生成器生成同一个属性名,编译报错。 所以之前才注释掉了。
对比:之前你同事的写法(传统方式)vs 现在的

// 传统方式(没有源生成器)
public class MainShellVM
{
    public ICommand StartScanCommand { get; }
    public MainShellVM()
    {
        StartScanCommand = new RelayCommand(StartScan);
    }
    private void StartScan() { ... }
}

// 现在用 [RelayCommand] 的方式
public partial class MainShellVM : ObservableObject
{
    private void StartScan() { ... }
    // → StartScanCommand 自动生成了
}

效果完全一样,只是少写了几行模板代码。

问题6

public sealed class UpdateTargetMessage
{
public double NewTargetX { get; }
public string ScanId { get; }
public UpdateTargetMessage(double newTargetX,string scanId = null)
{
NewTargetX = newTargetX;
ScanId = scanId;
}
}

这个属性里为啥要放个构造方法,不直接用set
好问题!这涉及到消息类为什么要设计成不可变。
对比两种写法
你问的写法(可变的):

public sealed class UpdateTargetMessage
{
    public double NewTargetX { get; set; }   // ← 有 set
    public string ScanId { get; set; }

    // 构造函数可选,不必须
}

发消息时:
csharp复制

var msg = new UpdateTargetMessage();
msg.NewTargetX = 80;
msg.ScanId = _currentScanId;
WeakReferenceMessenger.Default.Send(msg);

现在项目里的写法(不可变的):
csharp复制

public sealed class UpdateTargetMessage
{
    public double NewTargetX { get; }        // ← 只有 get,没有 set
    public string ScanId { get; }

    public UpdateTargetMessage(double newTargetX, string scanId = null)  // ← 必须通过构造函数赋值
    {
        NewTargetX = newTargetX;
        ScanId = scanId;
    }
}

发消息时:

WeakReferenceMessenger.Default.Send(new UpdateTargetMessage(TargetX, _currentScanId));
// 创建完就不能改了

为什么消息要设计成不可变
1️⃣ 线程安全
WeakReferenceMessenger 会把消息同时发给多个订阅者,可能在不同线程执行:
code复制MainShellVM 发消息

AxisEcatVM 收到(后台线程)
XrayImageVM 收到(UI线程)
如果消息是可变的,AxisEcatVM 正在读的时候,XrayImageVM 把它改了——竞态条件(Race Condition),数据就乱了。
不可变对象:创建后只读,多线程同时读,绝对安全。
2️⃣ 消息代表"已经发生的事实"
code复制UpdateTargetMessage(80, “AAA”)
= “扫描AAA,目标改成了80”

这个事实发出去之后,就不应该再变了。
如果有人中途改成 NewTargetX = 999,
后面收到的订阅者看到的就是错误的数据。
3️⃣ sealed + 不可变 = 完整保护
csharp复制

public sealed class UpdateTargetMessage  // ← sealed:不能被继承篡改
{
    public double NewTargetX { get; }   // ← 只有 get:不能改
}

现代 C# 的更简洁写法
如果是 C# 9.0+,可以用 record(天生不可变):

public sealed record UpdateTargetMessage(
    double NewTargetX, 
    string? ScanId = null
);
// 一行搞定,属性默认只读,构造函数自动生成

你这个项目用的是传统写法,效果一样。
一句话总结: 消息类用构造函数 + 只读属性,是为了线程安全 + 语义正确(消息是事实,事实不能改)。

问题7

lock (_scanLock)锁啥
翻译:

private void StartScanInternal(double? targetX = null, double? targetY = null, int? exposureMs = null,bool force=false,string scanId=null)
{
//throw new NotImplementedException();
lock (_scanLock)
{
if (_isDisposed) return;
if (_runningScanTask != null && !_runningScanTask.IsCompleted )
{
if(!force) {
WeakReferenceMessenger.Default.Send(new LogMessage(“已有扫描任务运行,忽略新的请求”));
return;
}
try { _cts?.Cancel(); } catch { }
}
_activeScanId= scanId;
_cts = new CancellationTokenSource();
var token=_cts.Token;
double tx = targetX ?? TargetX;
double ty = targetY ?? TargetY;
_runningScanTask = RunScanTask(tx, ty, exposureMs, _cts.Token);
_runningScanTask.ContinueWith(t => {
if (t.IsFaulted)
{
var ex=t.Exception?.GetBaseException();
try { WeakReferenceMessenger.Default.Send(new LogMessage($“扫描任务异常:{ex?.Message})); }catch { }
}
},TaskScheduler.Default );
}
} lock (_scanLock)锁啥

lock (_scanLock) 保护的是三个共享字段在这段代码里的读写安全:

private readonly object _scanLock = new object();   // 专门的锁对象

// 三个被保护的字段
private CancellationTokenSource _cts;               // ① 取消令牌
private Task _runningScanTask;                      // ② 当前运行的扫描任务
private string _activeScanId;                       // ③ 当前扫描ID

为什么需要锁——多线程同时访问
在这里插入图片描述
没有锁的话会发生这种事:

时间线(无锁的灾难):

线程A(Stop)                      线程B(StartScanInternal)
────────────────                   ────────────────────────
if (_runningScanTask != null)       if (_runningScanTask != null)
  → true(还没判断完)               → true(刚判断完)
  ↓                                ↓
_cts?.Cancel()                     _cts?.Cancel()     ← 两次取消同一个CTS
  ↓                                ↓
_cts = new CTS()                   _runningScanTask = 新任务
  ↓                                ↓
_runningScanTask = 新任务          _activeScanId = "BBB"
  ↓
_activeScanId = "AAA"

线程A: _activeScanId 设成 "AAA"
线程B: _activeScanId 设成 "BBB"
最后两个线程互相覆盖,_activeScanId 到底是谁?不知道!

锁保护的完整临界区

lock (_scanLock)   // ← 进门前抢锁
{
    // ★ 在这里面读/写 _runningScanTask / _cts / _activeScanId 都是安全的
    if (_isDisposed) return;
    
    if (_runningScanTask != null && !_runningScanTask.IsCompleted)
    {
        if (!force) { return; }   // ← 挡住重复请求
        _cts?.Cancel();            // ← 取消旧任务(只有一个线程能执行到这里)
    }
    
    _activeScanId = scanId;       // ← 赋值(不会被其他线程同时改)
    _cts = new CancellationTokenSource();
    _runningScanTask = RunScanTask(...);   // ← 启动新任务
}
// ← 出门时自动释放锁,其他线程可以进来

_scanLock 的作用:保证 _runningScanTask、_cts、_activeScanId 这三个共享字段,在"启动任务→运行中→停止/结束→清理"整个生命周期里,每次只有一个线程在操作它们。

重复取消_cts.Cancel()

_cts = new CancellationTokenSource();   // 创建一个 CTS

_cts.Cancel();                          // 第1次取消:发送取消信号
_cts.Cancel();                          // 第2次取消:再次调用

第1次 Cancel() → 遍历所有已注册 Token.Register() 的回调,一一执行
第2次 Cancel() → InvalidOperationException: Cancel has already been called on this object

时刻 T1:Stop() 线程进来
  _cts?.Cancel();   // ← 第1次取消,_cts.Token.Register() 注册的回调被触发
                    //   回调是 RunScanTask 里的 await Task.Delay(200, token)
                    //   → 抛出 OperationCanceledException
                    //   → RunScanTask 里的 catch 捕获
  ↓
 还没来得及赋值新 CTS
  
时刻 T2:StartScanInternal() 线程进来
  if (_runningScanTask != null && !_runningScanTask.IsCompleted)
  → true(任务还在取消中)
  ↓
  _cts?.Cancel();   // ← _cts 还是同一个对象!第2次调用
  → InvalidOperationException: Cancel has already been called
  
  或者:
  
  _cts = new CTS(); // ← 直接换新对象
  
  // 但如果还没来得及换,Stop() 线程继续执行:
  _runningScanTask = 新任务;

回调

回调是把一段代码当作参数传给另一个方法,让它在合适的时机被调用。
生活中的类比
你点外卖时跟服务员说:
“菜做好了叫我”(留了你的电话号码)

你不用站在厨房门口等
厨房做完菜 → 自动打你电话 → 你收到通知
这就是回调:你不在原地等,而是留个"通知我"的钩子,等事件发生时会自动触发。
你项目里的 Timer 回调

_statusTimer = new System.Threading.Timer(
    RefreshAxisStatus,   // ← 这是一个方法
    null,
    0,
    200
);

这里把 RefreshAxisStatus(一整段代码)传给了 Timer。

Timer 内部大概是这样:

class Timer
{
    private Action<object?> _callback;   // 存着你传进来的方法

    internal void Start()
    {
        while (true)
        {
            Thread.Sleep(200);
            _callback(null);   // ← 每200ms,自动调用你写的 RefreshAxisStatus()
        }
    }
}

另一个回调

_cts = new CancellationTokenSource();

Task.Delay(200, _cts.Token);   // Task.Delay 内部藏着一个回调

Task.Delay内部

public static Task Delay(int ms, CancellationToken token)
{
    var tcs = new TaskCompletionSource<bool>();
    
    // 这里藏着一个回调:取消时执行
    token.Register(() =>              // ← 你传进来一个 Action
    {
        tcs.TrySetCanceled();         // ← 取消时自动触发这个回调
    });
    
    return tcs.Task;
}

当调用 _cts.Cancel() 时,自动触发你传进去的那个回调。
回调的三种写法对比
在这里插入图片描述

// 委托变量
WeakReferenceMessenger.Default.Register<StartScanMessage>(this, (r, m) => { ... });
//                                          ↑ Lambda

// System.Threading.Timer 的构造函数第二个参数也是回调
_statusTimer = new System.Threading.Timer(RefreshAxisStatus, null, 0, 200);
//                                             ↑ 委托变量

回调 = 把"你要做的事情"包装成数据传进去,等时机到了由对方替你调用。

其他



Admin(无密码)@Admin MINGW64 ~
$ cd "/D/Download/WpfApp6 (2)/"

Admin(无密码)@Admin MINGW64 /D/Download/WpfApp6 (2)
$ cd wpfapp6

Admin(无密码)@Admin MINGW64 /D/Download/WpfApp6 (2)/wpfapp6 (master)
$ git add .

Admin(无密码)@Admin MINGW64 /D/Download/WpfApp6 (2)/wpfapp6 (master)
$ git commit -m "部分Bug修复"
[master ab357b0] 部分Bug修复
 7 files changed, 189 insertions(+), 17 deletions(-)
 delete mode 100644 .vs/WpfApp6/FileContentIndex/8396a965-6956-4e6c-963a-b7ddad38be4d.vsidx

Admin(无密码)@Admin MINGW64 /D/Download/WpfApp6 (2)/wpfapp6 (master)
$ git push origin master
Enumerating objects: 26, done.
Counting objects: 100% (26/26), done.
Delta compression using up to 12 threads
Compressing objects: 100% (14/14), done.
Writing objects: 100% (14/14), 8.05 KiB | 2.68 MiB/s, done.
Total 14 (delta 11), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Powered by GITEE.COM [1.1.23]
remote: Set trace flag b749cb95
To https://gitee.com/CZX1140075663/wpfapp6-test
   0c771ac..ab357b0  master -> master

Admin(无密码)@Admin MINGW64 /D/Download/WpfApp6 (2)/wpfapp6 (master)
$ git add .

Admin(无密码)@Admin MINGW64 /D/Download/WpfApp6 (2)/wpfapp6 (master)
$ git commit -m "部分Bug修复"
[master 27c90d6] 部分Bug修复
 4 files changed, 261 insertions(+), 83 deletions(-)

Admin(无密码)@Admin MINGW64 /D/Download/WpfApp6 (2)/wpfapp6 (master)
$ git push origin master
Enumerating objects: 19, done.
Counting objects: 100% (19/19), done.
Delta compression using up to 12 threads
Compressing objects: 100% (10/10), done.
Writing objects: 100% (10/10), 5.17 KiB | 1.29 MiB/s, done.
Total 10 (delta 7), reused 0 (delta 0), pack-reused 0 (from 0)
remote: Powered by GITEE.COM [1.1.23]
remote: Set trace flag 204698a5
To https://gitee.com/CZX1140075663/wpfapp6-test
   ab357b0..27c90d6  master -> master

Admin(无密码)@Admin MINGW64 /D/Download/WpfApp6 (2)/wpfapp6 (master)
$

测试项目逻辑流程还在整理中:
在这里插入图片描述

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐