feat: 完善登录验证码、订单同步和打包功能

- 添加验证码登录支持(图形验证码显示和输入)
- 修复订单同步,正确解析收件人信息(从logisticses合并)
- 修复API端点配置(user.api.it120.cc)
- 添加Costura.Fody实现单文件EXE打包
- 添加应用图标(app.ico)
- 添加Inno Setup安装脚本(支持Win7+)
- 暂时禁用导入发货功能
- 添加.gitignore文件

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Administrator
2025-12-18 00:05:22 +08:00
parent 473b44510d
commit 0763a2623b
21 changed files with 876 additions and 181 deletions

75
.gitignore vendored Normal file
View File

@@ -0,0 +1,75 @@
# Build results
[Dd]ebug/
[Rr]elease/
x64/
x86/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio
.vs/
*.user
*.suo
*.userosscache
*.sln.docstates
*.userprefs
# NuGet
*.nupkg
**/[Pp]ackages/*
!**/[Pp]ackages/build/
*.nuget.props
*.nuget.targets
# Build outputs
publish/
installer/
*.exe
*.dll
*.pdb
*.cache
# Installer files
*.7z
*.zip
*.msi
# Test results
[Tt]est[Rr]esult*/
*.trx
# Rider
.idea/
*.sln.iml
# OS files
Thumbs.db
ehthumbs.db
Desktop.ini
.DS_Store
# Temp files
*.tmp
*.temp
*.log
*~
nul
# Local data
*.db
*.sqlite
# Config with secrets (keep App.config template)
# App.config
# Generated files
FodyWeavers.xsd
# Backup files
*.bak
*.orig
# API test files
*_response.json

View File

@@ -5,8 +5,8 @@
</startup>
<appSettings>
<!-- API工厂配置 -->
<add key="ApiBaseUrl" value="https://user.api.it120.cc" />
<add key="SubDomain" value="vv125s" />
<add key="ApiBaseUrl" value="https://api.it120.cc" />
<add key="SubDomain" value="let5see" />
<!-- 同步配置 -->
<add key="SyncPageSize" value="50" />

View File

@@ -1,5 +1,6 @@
using System;
using System.Data.SQLite;
using System.Diagnostics;
using System.IO;
using PackagingMallShipper.Helpers;
@@ -19,13 +20,21 @@ namespace PackagingMallShipper.Data
);
var dir = Path.GetDirectoryName(_dbPath);
Debug.WriteLine($"[数据库初始化] 数据库目录: {dir}");
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
Debug.WriteLine($"[数据库初始化] 创建数据库目录");
}
_connectionString = $"Data Source={_dbPath};Version=3;";
Debug.WriteLine($"[数据库初始化] 数据库路径: {_dbPath}");
Debug.WriteLine($"[数据库初始化] 数据库文件存在: {File.Exists(_dbPath)}");
if (!File.Exists(_dbPath))
{
Debug.WriteLine($"[数据库初始化] 创建新数据库文件");
SQLiteConnection.CreateFile(_dbPath);
CreateTables();
}
@@ -40,15 +49,146 @@ namespace PackagingMallShipper.Data
private static void CreateTables()
{
using (var conn = GetConnection())
try
{
conn.Open();
var sql = ResourceHelper.GetEmbeddedResource("schema.sql");
using (var cmd = new SQLiteCommand(sql, conn))
Debug.WriteLine($"[数据库初始化] 开始创建数据库表");
using (var conn = GetConnection())
{
cmd.ExecuteNonQuery();
conn.Open();
Debug.WriteLine($"[数据库初始化] 数据库连接已打开");
string sql;
try
{
// 尝试从嵌入资源读取
sql = ResourceHelper.GetEmbeddedResource("schema.sql");
Debug.WriteLine($"[数据库初始化] 从嵌入资源读取 schema.sql长度: {sql?.Length ?? 0} 字符");
}
catch (Exception ex)
{
// 如果嵌入资源读取失败,尝试从文件系统读取
Debug.WriteLine($"[数据库初始化] 嵌入资源读取失败: {ex.Message}");
var schemaPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Data", "Resources", "schema.sql");
Debug.WriteLine($"[数据库初始化] 尝试从文件读取: {schemaPath}");
if (!File.Exists(schemaPath))
{
// 如果文件也不存在使用硬编码的SQL
Debug.WriteLine($"[数据库初始化] schema.sql 文件不存在使用硬编码SQL");
sql = GetHardcodedSchema();
}
else
{
sql = File.ReadAllText(schemaPath);
Debug.WriteLine($"[数据库初始化] 从文件读取成功,长度: {sql?.Length ?? 0} 字符");
}
}
using (var cmd = new SQLiteCommand(sql, conn))
{
cmd.ExecuteNonQuery();
Debug.WriteLine($"[数据库初始化] 数据库表创建成功");
}
}
}
catch (Exception ex)
{
Debug.WriteLine($"[数据库初始化] 创建表失败: {ex.Message}");
Debug.WriteLine($"[数据库初始化] StackTrace: {ex.StackTrace}");
throw;
}
}
private static string GetHardcodedSchema()
{
return @"
-- 1. 本地用户会话
CREATE TABLE IF NOT EXISTS local_session (
id INTEGER PRIMARY KEY,
mobile TEXT NOT NULL,
token TEXT NOT NULL,
uid INTEGER,
nickname TEXT,
enterprise_id INTEGER,
last_login_at DATETIME DEFAULT CURRENT_TIMESTAMP,
token_expires_at DATETIME
);
-- 2. 订单缓存表
CREATE TABLE IF NOT EXISTS orders_cache (
id INTEGER PRIMARY KEY,
order_number TEXT NOT NULL UNIQUE,
status INTEGER NOT NULL,
amount REAL,
amount_real REAL,
uid INTEGER,
user_mobile TEXT,
logistics_name TEXT,
logistics_mobile TEXT,
logistics_province TEXT,
logistics_city TEXT,
logistics_district TEXT,
logistics_address TEXT,
goods_json TEXT,
express_company_id INTEGER,
express_company_name TEXT,
tracking_number TEXT,
date_ship DATETIME,
sync_status TEXT DEFAULT 'synced',
local_updated_at DATETIME,
date_add DATETIME,
date_pay DATETIME,
date_update DATETIME,
synced_at DATETIME
);
-- 3. 发货队列表
CREATE TABLE IF NOT EXISTS ship_queue (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER NOT NULL,
order_number TEXT NOT NULL,
express_company_id INTEGER NOT NULL,
tracking_number TEXT NOT NULL,
status TEXT DEFAULT 'pending',
retry_count INTEGER DEFAULT 0,
error_message TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME
);
-- 4. 同步日志表
CREATE TABLE IF NOT EXISTS sync_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
sync_type TEXT NOT NULL,
sync_mode TEXT,
sync_start DATETIME NOT NULL,
sync_end DATETIME,
total_count INTEGER DEFAULT 0,
new_count INTEGER DEFAULT 0,
updated_count INTEGER DEFAULT 0,
failed_count INTEGER DEFAULT 0,
status TEXT NOT NULL,
error_message TEXT
);
-- 5. 操作日志表
CREATE TABLE IF NOT EXISTS operation_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
operation_type TEXT NOT NULL,
target_id INTEGER,
target_number TEXT,
details TEXT,
result TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
-- 索引
CREATE INDEX IF NOT EXISTS idx_orders_status ON orders_cache(status);
CREATE INDEX IF NOT EXISTS idx_orders_date_add ON orders_cache(date_add);
CREATE INDEX IF NOT EXISTS idx_orders_order_number ON orders_cache(order_number);
CREATE INDEX IF NOT EXISTS idx_ship_queue_status ON ship_queue(status);
CREATE INDEX IF NOT EXISTS idx_sync_logs_status ON sync_logs(status);
";
}
public static void ExecuteNonQuery(string sql, params SQLiteParameter[] parameters)

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
<Costura>
<!-- 设置为true以包含调试符号 -->
<IncludeDebugSymbols>false</IncludeDebugSymbols>
<!-- 禁用压缩可提高启动速度 -->
<DisableCompression>false</DisableCompression>
<!-- 创建临时目录存放原生DLL -->
<CreateTemporaryAssemblies>true</CreateTemporaryAssemblies>
</Costura>
</Weavers>

View File

@@ -25,6 +25,18 @@ namespace PackagingMallShipper.Models
public int Uid { get; set; }
}
public class AdminLoginData
{
[JsonProperty("id")]
public int Id { get; set; }
[JsonProperty("token")]
public string Token { get; set; }
[JsonProperty("nickname")]
public string Nickname { get; set; }
}
public class UserInfo
{
[JsonProperty("nick")]
@@ -43,18 +55,122 @@ namespace PackagingMallShipper.Models
public string Token { get; set; }
public string Message { get; set; }
public UserInfo UserInfo { get; set; }
public bool RequireCaptcha { get; set; } // 是否需要验证码
}
public class OrderListData
{
// API可能返回不同的字段名
[JsonProperty("orderList")]
public List<OrderDto> OrderList { get; set; }
[JsonProperty("result")]
public List<OrderDto> Result { get; set; }
[JsonProperty("list")]
public List<OrderDto> List { get; set; }
// 物流/收件人信息列表与订单通过id关联
[JsonProperty("logisticses")]
public List<LogisticsInfo> Logisticses { get; set; }
// 商品列表与订单通过orderId关联
[JsonProperty("goods")]
public List<GoodsItem> Goods { get; set; }
[JsonProperty("totalRow")]
public int TotalRow { get; set; }
[JsonProperty("total")]
public int Total { get; set; }
[JsonProperty("totalPage")]
public int TotalPage { get; set; }
// 辅助方法:获取订单列表(兼容多种返回格式)并合并物流信息
public List<OrderDto> GetOrders()
{
var orders = Result ?? OrderList ?? List ?? new List<OrderDto>();
// 合并物流信息到订单
if (Logisticses != null && Logisticses.Count > 0)
{
var logisticsMap = new Dictionary<int, LogisticsInfo>();
foreach (var log in Logisticses)
{
logisticsMap[log.Id] = log;
}
foreach (var order in orders)
{
if (logisticsMap.TryGetValue(order.Id, out var logistics))
{
order.LogisticsName = logistics.LinkMan;
order.LogisticsMobile = logistics.Mobile;
order.LogisticsProvince = logistics.ProvinceStr;
order.LogisticsCity = logistics.CityStr;
order.LogisticsDistrict = logistics.AreaStr;
order.LogisticsAddress = logistics.Address;
}
}
}
// 合并商品信息到订单
if (Goods != null && Goods.Count > 0)
{
var goodsMap = new Dictionary<int, List<GoodsItem>>();
foreach (var item in Goods)
{
if (!goodsMap.ContainsKey(item.OrderId))
goodsMap[item.OrderId] = new List<GoodsItem>();
goodsMap[item.OrderId].Add(item);
}
foreach (var order in orders)
{
if (goodsMap.TryGetValue(order.Id, out var items))
{
order.Goods = items;
}
}
}
return orders;
}
// 辅助方法:获取总记录数
public int GetTotalRow()
{
return TotalRow > 0 ? TotalRow : Total;
}
}
// 物流/收件人信息
public class LogisticsInfo
{
[JsonProperty("id")]
public int Id { get; set; } // 订单ID
[JsonProperty("linkMan")]
public string LinkMan { get; set; } // 收件人
[JsonProperty("mobile")]
public string Mobile { get; set; } // 收件人电话
[JsonProperty("provinceStr")]
public string ProvinceStr { get; set; }
[JsonProperty("cityStr")]
public string CityStr { get; set; }
[JsonProperty("areaStr")]
public string AreaStr { get; set; }
[JsonProperty("address")]
public string Address { get; set; }
[JsonProperty("streetStr")]
public string StreetStr { get; set; }
}
public class OrderDto

View File

@@ -63,14 +63,29 @@ namespace PackagingMallShipper.Models
public class GoodsItem
{
[JsonProperty("orderId")]
public int OrderId { get; set; }
[JsonProperty("goodsId")]
public int GoodsId { get; set; }
[JsonProperty("goodsName")]
public string GoodsName { get; set; }
[JsonProperty("number")]
public int Number { get; set; }
[JsonProperty("price")]
[JsonProperty("amount")]
public decimal Amount { get; set; }
[JsonProperty("amountSingle")]
public decimal Price { get; set; }
[JsonProperty("pic")]
public string Pic { get; set; }
[JsonProperty("unit")]
public string Unit { get; set; }
}
public static class OrderStatus

View File

@@ -1,139 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}</ProjectGuid>
<OutputType>WinExe</OutputType>
<TargetFramework>net48</TargetFramework>
<RootNamespace>PackagingMallShipper</RootNamespace>
<AssemblyName>PackagingMallShipper</AssemblyName>
<TargetFrameworkVersion>v4.8</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<WarningLevel>4</WarningLevel>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<Deterministic>true</Deterministic>
<LangVersion>7.3</LangVersion>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<PlatformTarget>AnyCPU</PlatformTarget>
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup>
<UseWPF>true</UseWPF>
<UseWindowsForms>false</UseWindowsForms>
<LangVersion>9.0</LangVersion>
<ApplicationIcon>Resources\Icons\app.ico</ApplicationIcon>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<!-- 版本信息 -->
<Version>1.0.0</Version>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Company>PackagingMall</Company>
<Product>包装商城发货助手</Product>
<Copyright>Copyright © 2025</Copyright>
</PropertyGroup>
<ItemGroup>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Net.Http" />
<Reference Include="System.Xml" />
<Reference Include="System.Xaml">
<RequiredTargetFramework>4.0</RequiredTargetFramework>
</Reference>
<Reference Include="WindowsBase" />
<Reference Include="PresentationCore" />
<Reference Include="PresentationFramework" />
</ItemGroup>
<ItemGroup>
<ApplicationDefinition Include="App.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</ApplicationDefinition>
<Page Include="Views\MainWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\LoginWindow.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\OrderListView.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\ShippingDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Resources\Styles.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Compile Include="App.xaml.cs">
<DependentUpon>App.xaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Include="Converters\BoolToVisibilityConverter.cs" />
<Compile Include="Converters\StatusToColorConverter.cs" />
<Compile Include="Converters\StatusToTextConverter.cs" />
<Compile Include="Data\SqliteHelper.cs" />
<Compile Include="Helpers\AppConfig.cs" />
<Compile Include="Helpers\ExpressCompanies.cs" />
<Compile Include="Helpers\ResourceHelper.cs" />
<Compile Include="Models\ApiResponses.cs" />
<Compile Include="Models\LocalSession.cs" />
<Compile Include="Models\Order.cs" />
<Compile Include="Models\ShipModels.cs" />
<Compile Include="Models\SyncLog.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Services\AuthService.cs" />
<Compile Include="Services\ExcelService.cs" />
<Compile Include="Services\Interfaces.cs" />
<Compile Include="Services\OrderService.cs" />
<Compile Include="Services\ShipService.cs" />
<Compile Include="Services\SyncService.cs" />
<Compile Include="ViewModels\LoginViewModel.cs" />
<Compile Include="ViewModels\MainViewModel.cs" />
<Compile Include="ViewModels\OrderListViewModel.cs" />
<Compile Include="ViewModels\ViewModelBase.cs" />
<Compile Include="Views\LoginWindow.xaml.cs">
<DependentUpon>LoginWindow.xaml</DependentUpon>
</Compile>
<Compile Include="Views\MainWindow.xaml.cs">
<DependentUpon>MainWindow.xaml</DependentUpon>
</Compile>
<Compile Include="Views\OrderListView.xaml.cs">
<DependentUpon>OrderListView.xaml</DependentUpon>
</Compile>
<Compile Include="Views\ShippingDialog.xaml.cs">
<DependentUpon>ShippingDialog.xaml</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Data\Resources\schema.sql" />
</ItemGroup>
<ItemGroup>
<Resource Include="Resources\Icons\app.ico" />
</ItemGroup>
<ItemGroup>
<None Include="App.config" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="System.Data.SQLite" Version="1.0.118" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="ClosedXML" Version="0.102.2" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
<!-- 将所有DLL打包到EXE中 -->
<PackageReference Include="Costura.Fody" Version="5.7.0" PrivateAssets="all" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<ItemGroup>
<Reference Include="System.Configuration" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Include="Data\Resources\schema.sql" />
</ItemGroup>
<ItemGroup>
<None Update="App.config">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -0,0 +1,60 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<linearGradient id="boxGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#FF9800;stop-opacity:1" />
<stop offset="100%" style="stop-color:#F57C00;stop-opacity:1" />
</linearGradient>
<linearGradient id="boxSide" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#E65100;stop-opacity:1" />
<stop offset="100%" style="stop-color:#EF6C00;stop-opacity:1" />
</linearGradient>
<linearGradient id="boxTop" x1="0%" y1="100%" x2="0%" y2="0%">
<stop offset="0%" style="stop-color:#FFB74D;stop-opacity:1" />
<stop offset="100%" style="stop-color:#FFA726;stop-opacity:1" />
</linearGradient>
<linearGradient id="arrowGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" style="stop-color:#4CAF50;stop-opacity:1" />
<stop offset="100%" style="stop-color:#388E3C;stop-opacity:1" />
</linearGradient>
</defs>
<!-- 背景圆形 -->
<circle cx="256" cy="256" r="240" fill="#FFF3E0"/>
<!-- 包装盒主体 - 正面 -->
<path d="M100 200 L256 280 L256 420 L100 340 Z" fill="url(#boxGradient)"/>
<!-- 包装盒主体 - 侧面 -->
<path d="M256 280 L412 200 L412 340 L256 420 Z" fill="url(#boxSide)"/>
<!-- 包装盒顶部 -->
<path d="M100 200 L256 120 L412 200 L256 280 Z" fill="url(#boxTop)"/>
<!-- 盒子封口线 -->
<line x1="256" y1="120" x2="256" y2="280" stroke="#E65100" stroke-width="3"/>
<line x1="100" y1="200" x2="412" y2="200" stroke="#E65100" stroke-width="2"/>
<!-- 胶带 -->
<rect x="220" y="105" width="72" height="25" rx="3" fill="#8D6E63"/>
<rect x="224" y="109" width="64" height="17" rx="2" fill="#A1887F"/>
<!-- 发货箭头 -->
<g transform="translate(320, 80)">
<!-- 箭头主体 -->
<path d="M0 60 L40 60 L40 40 L80 70 L40 100 L40 80 L0 80 Z" fill="url(#arrowGradient)"/>
<!-- 箭头高光 -->
<path d="M0 60 L40 60 L40 40 L50 50 L10 50 L10 70 L0 70 Z" fill="#66BB6A" opacity="0.5"/>
</g>
<!-- 商城标志 - 购物车图标简化版 -->
<g transform="translate(130, 290)">
<circle cx="20" cy="60" r="8" fill="#5D4037"/>
<circle cx="50" cy="60" r="8" fill="#5D4037"/>
<path d="M5 10 L15 10 L25 45 L55 45 L60 25 L20 25" stroke="#5D4037" stroke-width="5" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<!-- 文字装饰线 -->
<rect x="310" y="300" width="60" height="6" rx="3" fill="#FFCC80"/>
<rect x="310" y="315" width="45" height="6" rx="3" fill="#FFCC80"/>
<rect x="310" y="330" width="55" height="6" rx="3" fill="#FFCC80"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -1,5 +1,6 @@
using System;
using System.Data.SQLite;
using System.Diagnostics;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
@@ -25,43 +26,74 @@ namespace PackagingMallShipper.Services
public bool IsLoggedIn => !string.IsNullOrEmpty(GetToken());
public async Task<LoginResult> LoginAsync(string mobile, string password)
public string GetCaptchaUrl(string key)
{
return $"https://user.api.it120.cc/code?k={Uri.EscapeDataString(key)}";
}
public async Task<LoginResult> LoginAsync(string userName, string password, string captcha = null, string captchaKey = null)
{
try
{
var url = AppConfig.GetApiUrl($"/user/m/login?mobile={mobile}&pwd={password}");
// 使用子账号登录接口 /login/userName/v2
// 注意:该接口会优先判断手机号码登录,如果满足直接登录成功,其次才会尝试子账号登录
// 该接口要求必须提供验证码参数
var urlBuilder = new System.Text.StringBuilder();
urlBuilder.Append("https://user.api.it120.cc/login/userName/v2");
urlBuilder.Append($"?userName={Uri.EscapeDataString(userName)}");
urlBuilder.Append($"&pwd={Uri.EscapeDataString(password)}");
urlBuilder.Append($"&pdomain={Uri.EscapeDataString(AppConfig.SubDomain)}");
urlBuilder.Append("&rememberMe=true");
// 验证码参数是必须的
urlBuilder.Append($"&imgcode={Uri.EscapeDataString(captcha ?? "")}");
urlBuilder.Append($"&k={Uri.EscapeDataString(captchaKey ?? "")}");
var url = urlBuilder.ToString();
// 详细日志请求URL隐藏密码
var logUrl = url.Replace($"&pwd={Uri.EscapeDataString(password)}", "&pwd=***");
System.Diagnostics.Debug.WriteLine($"[登录请求] URL: {logUrl}");
System.Diagnostics.Debug.WriteLine($"[登录请求] 用户名: {userName}");
System.Diagnostics.Debug.WriteLine($"[登录请求] 验证码: {captcha}");
System.Diagnostics.Debug.WriteLine($"[登录请求] 验证码Key: {captchaKey}");
System.Diagnostics.Debug.WriteLine($"[登录请求] 子域名: {AppConfig.SubDomain}");
var response = await _httpClient.PostAsync(url, null);
var json = await response.Content.ReadAsStringAsync();
var result = JsonConvert.DeserializeObject<ApiResponse<LoginData>>(json);
// 详细日志API响应
System.Diagnostics.Debug.WriteLine($"[登录响应] HTTP状态码: {response.StatusCode}");
System.Diagnostics.Debug.WriteLine($"[登录响应] JSON原始内容: {json}");
var result = JsonConvert.DeserializeObject<ApiResponse<AdminLoginData>>(json);
System.Diagnostics.Debug.WriteLine($"[登录响应] 解析结果 - Code: {result?.Code}, Msg: {result?.Msg}");
if (result?.Code != 0)
{
System.Diagnostics.Debug.WriteLine($"[登录失败] 错误码: {result?.Code}, 错误信息: {result?.Msg}");
return new LoginResult
{
Success = false,
Message = result?.Msg ?? "登录失败"
Message = result?.Msg ?? "登录失败",
RequireCaptcha = result?.Code == 700 // 700 表示需要验证码
};
}
var token = result.Data.Token;
var uid = result.Data.Uid;
var adminId = result.Data.Id;
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("X-Token", token);
var userInfoUrl = AppConfig.GetApiUrl("/user/detail");
var userResponse = await _httpClient.GetAsync(userInfoUrl);
var userJson = await userResponse.Content.ReadAsStringAsync();
var userResult = JsonConvert.DeserializeObject<ApiResponse<UserInfo>>(userJson);
Debug.WriteLine($"[登录成功] Token: {token?.Substring(0, Math.Min(20, token?.Length ?? 0))}..., AdminId: {adminId}");
_currentSession = new LocalSession
{
Id = 1,
Mobile = mobile,
Mobile = userName, // 存储用户名/手机号
Token = token,
Uid = uid,
Nickname = userResult?.Data?.Nick ?? mobile,
Uid = adminId,
Nickname = result.Data.Nickname ?? userName,
LastLoginAt = DateTime.Now,
TokenExpiresAt = DateTime.Now.AddHours(AppConfig.TokenExpireHours)
};
@@ -72,11 +104,14 @@ namespace PackagingMallShipper.Services
{
Success = true,
Token = token,
UserInfo = userResult?.Data
UserInfo = null // 子账号登录不需要额外获取用户信息
};
}
catch (Exception ex)
{
Debug.WriteLine($"[登录异常] {ex.GetType().Name}: {ex.Message}");
Debug.WriteLine($"[登录异常] StackTrace: {ex.StackTrace}");
return new LoginResult
{
Success = false,

View File

@@ -49,8 +49,9 @@ namespace PackagingMallShipper.Services
worksheet.Cell(row, 1).Value = order.OrderNumber;
worksheet.Cell(row, 2).Value = order.DateAdd?.ToString("yyyy-MM-dd HH:mm:ss");
worksheet.Cell(row, 3).Value = order.LogisticsName;
worksheet.Cell(row, 4).Value = order.LogisticsMobile;
worksheet.Cell(row, 4).SetDataType(XLDataType.Text);
var mobileCell = worksheet.Cell(row, 4);
mobileCell.Value = order.LogisticsMobile;
mobileCell.Style.NumberFormat.Format = "@";
worksheet.Cell(row, 5).Value = order.LogisticsProvince;
worksheet.Cell(row, 6).Value = order.LogisticsCity;
worksheet.Cell(row, 7).Value = order.LogisticsDistrict;

View File

@@ -8,7 +8,8 @@ namespace PackagingMallShipper.Services
{
public interface IAuthService
{
Task<LoginResult> LoginAsync(string mobile, string password);
Task<LoginResult> LoginAsync(string userName, string password, string captcha = null, string captchaKey = null);
string GetCaptchaUrl(string key);
string GetToken();
bool IsLoggedIn { get; }
LocalSession CurrentSession { get; }

View File

@@ -1,3 +1,4 @@
using System;
using System.Collections.Generic;
using System.Data.SQLite;
using System.Threading.Tasks;

View File

@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Data.SQLite;
using System.Diagnostics;
using System.Net.Http;
using System.Threading.Tasks;
using Newtonsoft.Json;
@@ -57,22 +58,44 @@ namespace PackagingMallShipper.Services
{
OnSyncMessage?.Invoke($"正在同步第 {page}/{totalPages} 页...");
var url = AppConfig.GetApiUrl($"/order/list?page={page}&pageSize={pageSize}");
// 使用正确的API端点/user/apiExtOrder/list (POST请求)
// 域名user.api.it120.cc路径不含subdomain
var urlBuilder = new System.Text.StringBuilder();
urlBuilder.Append("https://user.api.it120.cc/user/apiExtOrder/list");
urlBuilder.Append($"?page={page}&pageSize={pageSize}");
urlBuilder.Append("&export=true"); // 获取更丰富的数据
if (lastSyncTime.HasValue)
url += $"&dateUpdateBegin={lastSyncTime:yyyy-MM-dd HH:mm:ss}";
urlBuilder.Append($"&dateUpdateBegin={lastSyncTime:yyyy-MM-dd HH:mm:ss}");
var url = urlBuilder.ToString();
Debug.WriteLine($"[同步请求] URL: {url}");
Debug.WriteLine($"[同步请求] Token: {token?.Substring(0, Math.Min(20, token?.Length ?? 0))}...");
_httpClient.DefaultRequestHeaders.Clear();
_httpClient.DefaultRequestHeaders.Add("X-Token", token);
var response = await _httpClient.GetAsync(url);
// 使用 POST 请求
var response = await _httpClient.PostAsync(url, null);
var json = await response.Content.ReadAsStringAsync();
Debug.WriteLine($"[同步响应] HTTP状态码: {response.StatusCode}");
var data = JsonConvert.DeserializeObject<ApiResponse<OrderListData>>(json);
if (data?.Code != 0)
{
if (data?.Msg?.Contains("token") == true || data?.Msg?.Contains("登录") == true)
throw new UnauthorizedAccessException($"当前登录token无效请重新登录");
throw new Exception(data?.Msg ?? "获取订单失败");
}
var orders = data.Data?.OrderList ?? new List<OrderDto>();
// 使用辅助方法获取订单列表(自动合并物流和商品信息)
var orders = data.Data?.GetOrders() ?? new List<OrderDto>();
totalPages = data.Data?.TotalPage ?? 1;
var totalRow = data.Data?.GetTotalRow() ?? 0;
Debug.WriteLine($"[同步响应] 订单数量: {orders.Count}, 总记录: {totalRow}, 总页数: {totalPages}");
if (orders.Count == 0) break;

View File

@@ -1,6 +1,7 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using PackagingMallShipper.Services;
@@ -10,33 +11,61 @@ namespace PackagingMallShipper.ViewModels
public partial class LoginViewModel : ViewModelBase
{
private readonly IAuthService _authService;
private string _captchaKey = "";
[ObservableProperty]
private string _mobile = "";
private string _userName = "";
[ObservableProperty]
private string _password = "";
[ObservableProperty]
private string _captcha = "";
[ObservableProperty]
private string _errorMessage = "";
[ObservableProperty]
private bool _rememberMe = true;
[ObservableProperty]
private bool _showCaptcha = false;
[ObservableProperty]
private BitmapImage _captchaImageSource;
public event Action OnLoginSuccess;
public LoginViewModel(IAuthService authService)
{
_authService = authService;
LoadSavedCredentials();
// 子账号登录必须提供验证码,默认显示
ShowCaptcha = true;
RefreshCaptcha();
}
[RelayCommand]
public void RefreshCaptcha()
{
_captchaKey = Guid.NewGuid().ToString("N");
var url = _authService.GetCaptchaUrl(_captchaKey);
CaptchaImageSource = new BitmapImage();
CaptchaImageSource.BeginInit();
CaptchaImageSource.UriSource = new Uri(url);
CaptchaImageSource.CacheOption = BitmapCacheOption.None;
CaptchaImageSource.CreateOptions = BitmapCreateOptions.IgnoreImageCache;
CaptchaImageSource.EndInit();
}
[RelayCommand]
private async Task LoginAsync()
{
if (string.IsNullOrWhiteSpace(Mobile))
if (string.IsNullOrWhiteSpace(UserName))
{
ErrorMessage = "请输入手机号";
ErrorMessage = "请输入用户名或手机号";
return;
}
@@ -46,12 +75,23 @@ namespace PackagingMallShipper.ViewModels
return;
}
if (ShowCaptcha && string.IsNullOrWhiteSpace(Captcha))
{
ErrorMessage = "请输入验证码";
return;
}
IsBusy = true;
ErrorMessage = "";
try
{
var result = await _authService.LoginAsync(Mobile, Password);
var result = await _authService.LoginAsync(
UserName,
Password,
ShowCaptcha ? Captcha : null,
ShowCaptcha ? _captchaKey : null
);
if (result.Success)
{
@@ -64,6 +104,19 @@ namespace PackagingMallShipper.ViewModels
else
{
ErrorMessage = result.Message ?? "登录失败";
// 如果需要验证码,显示验证码输入
if (result.RequireCaptcha && !ShowCaptcha)
{
ShowCaptcha = true;
RefreshCaptcha();
}
else if (ShowCaptcha)
{
// 验证码错误,刷新验证码
Captcha = "";
RefreshCaptcha();
}
}
}
catch (Exception ex)
@@ -80,10 +133,10 @@ namespace PackagingMallShipper.ViewModels
{
try
{
var mobile = Properties.Settings.Default.SavedMobile;
if (!string.IsNullOrEmpty(mobile))
var userName = Properties.Settings.Default.SavedMobile;
if (!string.IsNullOrEmpty(userName))
{
Mobile = mobile;
UserName = userName;
RememberMe = true;
}
}
@@ -97,7 +150,7 @@ namespace PackagingMallShipper.ViewModels
{
try
{
Properties.Settings.Default.SavedMobile = Mobile;
Properties.Settings.Default.SavedMobile = UserName;
Properties.Settings.Default.Save();
}
catch

View File

@@ -4,8 +4,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="包装商城发货助手 - 登录"
Height="400" Width="350"
Title="包装商城发货助手 - 登录"
Height="480" Width="350"
WindowStartupLocation="CenterScreen"
ResizeMode="NoResize"
Background="#F5F5F5">
@@ -22,19 +22,46 @@
HorizontalAlignment="Center"
Margin="0,0,0,30"/>
<TextBlock Text="手机号" Margin="0,0,0,5" FontSize="13"/>
<TextBox x:Name="MobileTextBox"
Text="{Binding Mobile, UpdateSourceTrigger=PropertyChanged}"
<TextBlock Text="用户名/手机号" Margin="0,0,0,5" FontSize="13"/>
<TextBox x:Name="UserNameTextBox"
Text="{Binding UserName, UpdateSourceTrigger=PropertyChanged}"
Height="35" FontSize="14" Padding="10,5"
Margin="0,0,0,15"/>
<TextBlock Text="密码" Margin="0,0,0,5" FontSize="13"/>
<PasswordBox x:Name="PasswordBox"
Height="35" FontSize="14" Padding="10,5"
Margin="0,0,0,10"
Margin="0,0,0,15"
PasswordChanged="PasswordBox_PasswordChanged"/>
<CheckBox Content="记住手机号"
<!-- 验证码区域 -->
<StackPanel x:Name="CaptchaPanel"
Visibility="{Binding ShowCaptcha, Converter={StaticResource BoolToVisibility}}"
Margin="0,0,0,15">
<TextBlock Text="验证码" Margin="0,0,0,5" FontSize="13"/>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="120"/>
</Grid.ColumnDefinitions>
<TextBox x:Name="CaptchaTextBox"
Text="{Binding Captcha, UpdateSourceTrigger=PropertyChanged}"
Height="35" FontSize="14" Padding="10,5"
Grid.Column="0" Margin="0,0,10,0"/>
<Border Grid.Column="1" BorderBrush="#CCCCCC" BorderThickness="1" CornerRadius="2">
<Image x:Name="CaptchaImage"
Source="{Binding CaptchaImageSource}"
Stretch="Fill"
MouseLeftButtonDown="CaptchaImage_Click"
Cursor="Hand"
ToolTip="点击刷新验证码"/>
</Border>
</Grid>
</StackPanel>
<CheckBox Content="记住用户名"
IsChecked="{Binding RememberMe}"
Margin="0,0,0,20"/>

View File

@@ -34,6 +34,11 @@ namespace PackagingMallShipper.Views
}
}
private void CaptchaImage_Click(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
_viewModel.RefreshCaptchaCommand.Execute(null);
}
private void OnLoginSuccess()
{
OpenMainWindow();

View File

@@ -77,10 +77,12 @@
Width="90" Height="30" Margin="0,0,5,0"
IsEnabled="{Binding IsBusy, Converter={StaticResource InverseBool}}"/>
<Button Content="📥 导入发货" Command="{Binding ImportAndShipCommand}"
<!-- 暂时禁用导入发货功能
<Button Content="📥 导入发货" Command="{Binding ImportAndShipCommand}"
Width="90" Height="30"
Background="#52C41A" Foreground="White" BorderThickness="0"
IsEnabled="{Binding IsBusy, Converter={StaticResource InverseBool}}"/>
-->
</StackPanel>
<!-- 订单列表 -->

View File

@@ -31,16 +31,28 @@
## 开发环境
- Visual Studio 2019 Community
- Visual Studio 2019+ 或 .NET SDK 8.0+
- .NET Framework 4.8 开发工具
## 编译步骤
1. 使用 Visual Studio 2019 打开 `PackagingMallShipper.sln`
### 方式一:命令行编译(推荐)
```bash
# 还原依赖并编译
dotnet build PackagingMallShipper.sln -c Release
# 输出文件位于
bin/Release/net48/PackagingMallShipper.exe
```
### 方式二Visual Studio
1. 使用 Visual Studio 2019+ 打开 `PackagingMallShipper.sln`
2. 还原 NuGet 包
3. 选择 `Release` 配置
4. 生成解决方案
5. 输出文件位于 `bin/Release/` 目录
5. 输出文件位于 `bin/Release/net48/` 目录
## 配置说明
@@ -49,8 +61,8 @@
```xml
<appSettings>
<!-- API工厂配置 -->
<add key="ApiBaseUrl" value="https://user.api.it120.cc" />
<add key="SubDomain" value="vv125s" />
<add key="ApiBaseUrl" value="https://api.it120.cc" />
<add key="SubDomain" value="let5see" />
<!-- 同步配置 -->
<add key="SyncPageSize" value="50" />
@@ -64,7 +76,11 @@
## 使用流程
1. **登录** - 使用API工厂账号登录
1. **登录** - 使用API工厂账号登录
- 输入用户名或手机号
- 输入密码
- 输入图形验证码(点击图片可刷新)
- 首次登录建议勾选"记住用户名"
2. **同步订单** - 点击"同步订单"获取最新订单
3. **查看订单** - 切换状态筛选,搜索订单
4. **发货方式一** - 导出Excel → 填写快递信息 → 导入发货

58
build_installer.bat Normal file
View File

@@ -0,0 +1,58 @@
@echo off
chcp 936 >nul
echo ========================================
echo PackagingMallShipper - Build Installer
echo ========================================
echo.
set "ISCC="
if exist "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" (
set "ISCC=C:\Program Files (x86)\Inno Setup 6\ISCC.exe"
)
if exist "C:\Program Files\Inno Setup 6\ISCC.exe" (
set "ISCC=C:\Program Files\Inno Setup 6\ISCC.exe"
)
if "%ISCC%"=="" (
echo [Error] Inno Setup 6 not found
echo.
echo Please download and install Inno Setup 6:
echo https://jrsoftware.org/isdl.php
echo.
pause
exit /b 1
)
echo [Info] Found Inno Setup: %ISCC%
echo.
if not exist "publish\PackagingMallShipper.exe" (
echo [Error] publish\PackagingMallShipper.exe not found
echo.
echo Please build Release version first:
echo dotnet build -c Release
echo.
pause
exit /b 1
)
echo [Info] Compiling installer...
echo.
"%ISCC%" setup.iss
if %ERRORLEVEL% EQU 0 (
echo.
echo ========================================
echo [Success] Installer created!
echo.
echo Location: installer\PackagingMallShipper_Setup_v1.0.0.exe
echo ========================================
echo.
if exist "installer" explorer "installer"
) else (
echo.
echo [Error] Build failed, error code: %ERRORLEVEL%
)
pause

148
setup.iss Normal file
View File

@@ -0,0 +1,148 @@
; Inno Setup Script for 包装商城发货助手
; Requires Inno Setup 6.x
#define MyAppName "包装商城发货助手"
#define MyAppVersion "1.0.0"
#define MyAppPublisher "PackagingMall"
#define MyAppExeName "PackagingMallShipper.exe"
[Setup]
; 应用程序信息
AppId={{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}
AppName={#MyAppName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppName}
AllowNoIcons=yes
; 输出设置
OutputDir=installer
OutputBaseFilename=PackagingMallShipper_Setup_v{#MyAppVersion}
; 压缩设置
Compression=lzma2/ultra64
SolidCompression=yes
; 权限设置
PrivilegesRequired=admin
; 系统要求 - Windows 7 SP1 或更高版本
MinVersion=6.1.7601
; 安装向导设置
WizardStyle=modern
DisableProgramGroupPage=yes
; 卸载设置
UninstallDisplayName={#MyAppName}
UninstallDisplayIcon={app}\{#MyAppExeName}
[Languages]
Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"
[Tasks]
Name: "desktopicon"; Description: "创建桌面快捷方式"; GroupDescription: "附加选项:"; Flags: checkedonce
[Files]
; 主程序
Source: "publish\PackagingMallShipper.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "publish\PackagingMallShipper.exe.config"; DestDir: "{app}"; Flags: ignoreversion
; SQLite 本地库
Source: "publish\x64\SQLite.Interop.dll"; DestDir: "{app}\x64"; Flags: ignoreversion
Source: "publish\x86\SQLite.Interop.dll"; DestDir: "{app}\x86"; Flags: ignoreversion
; .NET Framework 4.8 离线安装包(可选,取消注释以包含)
; Source: "redist\ndp48-x86-x64-allos-enu.exe"; DestDir: "{tmp}"; Flags: deleteafterinstall; Check: not IsDotNetInstalled
[Icons]
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{group}\卸载 {#MyAppName}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
; 安装完成后运行程序
Filename: "{app}\{#MyAppExeName}"; Description: "运行 {#MyAppName}"; Flags: nowait postinstall skipifsilent
[Code]
// 检查 .NET Framework 4.8 是否已安装
function IsDotNetInstalled: Boolean;
var
Release: Cardinal;
begin
Result := False;
// .NET Framework 4.8 的 Release 值为 528040 或更高
if RegQueryDWordValue(HKLM, 'SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full', 'Release', Release) then
begin
Result := (Release >= 528040);
end;
end;
// 获取 .NET Framework 版本描述
function GetDotNetVersionStr: String;
var
Release: Cardinal;
begin
Result := '未安装';
if RegQueryDWordValue(HKLM, 'SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full', 'Release', Release) then
begin
if Release >= 533320 then
Result := '4.8.1 或更高'
else if Release >= 528040 then
Result := '4.8'
else if Release >= 461808 then
Result := '4.7.2'
else if Release >= 461308 then
Result := '4.7.1'
else if Release >= 460798 then
Result := '4.7'
else if Release >= 394802 then
Result := '4.6.2'
else if Release >= 394254 then
Result := '4.6.1'
else if Release >= 393295 then
Result := '4.6'
else
Result := '4.5.x 或更低';
end;
end;
// 初始化安装向导
function InitializeSetup: Boolean;
var
ErrorCode: Integer;
NetVersion: String;
begin
Result := True;
// 检查 .NET Framework
if not IsDotNetInstalled then
begin
NetVersion := GetDotNetVersionStr;
if MsgBox('此程序需要 .NET Framework 4.8 才能运行。' + #13#10 + #13#10 +
'当前检测到的版本: ' + NetVersion + #13#10 + #13#10 +
'是否要打开微软官网下载 .NET Framework 4.8' + #13#10 +
'(下载完成后请先安装运行时,再重新运行此安装程序)',
mbConfirmation, MB_YESNO) = IDYES then
begin
// 打开 .NET Framework 4.8 下载页面
ShellExec('open', 'https://dotnet.microsoft.com/download/dotnet-framework/net48', '', '', SW_SHOW, ewNoWait, ErrorCode);
end;
Result := MsgBox('是否仍要继续安装?' + #13#10 + #13#10 +
'注意:如果没有安装 .NET Framework 4.8,程序将无法运行。',
mbConfirmation, MB_YESNO) = IDYES;
end;
end;
// 卸载时清理数据目录(可选)
procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep);
var
DataDir: String;
begin
if CurUninstallStep = usPostUninstall then
begin
DataDir := ExpandConstant('{localappdata}\PackagingMallShipper');
if DirExists(DataDir) then
begin
if MsgBox('是否删除程序数据(包括本地缓存的订单数据)?', mbConfirmation, MB_YESNO) = IDYES then
begin
DelTree(DataDir, True, True, True);
end;
end;
end;
end;