XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

STHApplication

【MAUI】寄当家——安卓端交易管理系统

公开
关注 0 Fork 0 Star 0
返回提交历史

XFEstudio/STHApplication

fix: lock server endpoint and verify Pixel 7 local integration

a93cf09
XFE工作室室长 <mail@xfegzs.com>
提交于

代码差异

21 个文件 +156 -43
Modified README.md +5 -3
@@ -16,7 +16,7 @@ Android .NET MAUI 应用与独立 ASP.NET Core 登录授权服务器。管理员
16 16 .\tools\start-server.ps1
17 17 ```
18 18
19 APP 登录页先「配置服务器地址」,再使用管理员账户登录。Android 模拟器使用 `http://10.0.2.2:5188`。USB 真机连接这台电脑时,可运行 Android SDK 的 `adb reverse tcp:5188 tcp:5188`,APP 填 `http://127.0.0.1:5188`。公网使用部署后的 HTTPS 地址;本项目没有向公网部署服务器。
19 服务器地址硬编码在 `src/JiDangJia/Services/ServerSettings.cs` 的 `BaseUrl` 常量中,APP 无修改入口,也不读取用户地址设置。当前本地测试构建固定为 `http://10.0.2.2:5188`,Android 模拟器直接连接这台电脑的服务器。部署到其他地址时只能修改该常量后重新编译;USB 真机调试可改为 `http://127.0.0.1:5188` 并使用 `adb reverse tcp:5188 tcp:5188`,正式发布改为部署后的 HTTPS 地址。本项目没有向公网部署服务器。
20 20
21 21 ## APP 功能
22 22
@@ -48,7 +48,7 @@ APP 登录页先「配置服务器地址」,再使用管理员账户登录。A
48 48
49 49 ```powershell
50 50 dotnet workload install maui-android
51 dotnet run --project tests/JiDangJia.Checks
51 dotnet run --project tests/JiDangJia.Checks -c Release
52 52 node tools/check-api.cjs
53 53 node tools/check-quotes.cjs
54 54 .\tools\publish-android.ps1
@@ -57,4 +57,6 @@ dotnet publish src/JiDangJia.Server -c Release -r win-x64 --self-contained true
57 57
58 58 `check-api.cjs` 使用独立测试服务器及数据库。`check-quotes.cjs` 使用真实行情连接,需要联网。APK 构建脚本首次生成专用签名密钥与密码文件,存放在 `artifacts/credentials/`,不包含在源码包中。升级同一安装应用时请继续使用该密钥。[微软 Android 发布说明](https://learn.microsoft.com/en-us/dotnet/maui/android/deployment/publish-cli?view=net-maui-10.0)说明了签名参数。
59 59
60 已通过 75 项业务、备份和授权检查,以及 24 项实际 HTTP 权限检查;已收到黄金、白银、铂金真实报价。当前电脑未启用安卓模拟器所需的硬件加速,模拟器启动未成功;尚未完成真机界面、拍照和打印验收。具体验收步骤在 `docs/验收步骤.md`。
60 已通过 75 项业务、备份和授权检查,以及 24 项实际 HTTP 权限检查。签名 Release APK 已在 Pixel 7 / Android 16 / API 36 模拟器连接本机 5188 服务器完成登录、卡密、管理员权限、行情、导航、回收及铺货入库、财务流水联调,记录与截图见 [本地联调测试](docs/本地联调测试.md)。拍照、实际打印和其他真机能力仍需按 [设备验收步骤](docs/验收步骤.md) 验收。
61
62 本项目关闭 `AndroidEnableMarshalMethods`,修复当前 Android 工具链在 Release 启动时出现的 `MauiApplication.n_onCreate` 链接错误,参见 [dotnet/maui #35209](https://github.com/dotnet/maui/issues/35209)。若从旧构建切换此属性,先执行 `dotnet clean src/JiDangJia/JiDangJia.csproj -c Release -f net10.0-android` 再发布。
Modified artifacts/SHA256.json +6 -6
@@ -1,17 +1,17 @@
1 1 [
2 2 {
3 3 "name": "寄当家-2.0.0.apk",
4 "bytes": 66132645,
5 "sha256": "2dec7059fc5b3a440ed503e867566df2afdb33d584a3bf54215413bd491a29e7"
4 "bytes": 65806091,
5 "sha256": "cd35b85e97902824d80220f6b7225b2d6a4cf8d5a4ca35b01b34fa0f87874b53"
6 6 },
7 7 {
8 8 "name": "寄当家-服务器-Windows.zip",
9 "bytes": 49591550,
10 "sha256": "3a92aa984d8666731f5dc1a698349489c17af892400be82fd9992ee8064031a0"
9 "bytes": 49592690,
10 "sha256": "072938181be14ea97ff0912f6de615bbaf2aa3c1ecdbf83cd194ca94905bdfbb"
11 11 },
12 12 {
13 13 "name": "寄当家-源码.zip",
14 "bytes": 345999,
15 "sha256": "09d09bb17c06423b9dfb70542744d460301cca11bf4f5efcbe9bf25a25524db0"
14 "bytes": 937222,
15 "sha256": "800e3c8aeb7d991a9ee57c58734503234bfc7e27c5b1062871c53a8ce091367c"
16 16 }
17 17 ]
Added artifacts/verification/android-home.png +0 -0
二进制文件已变更,无法进行逐行预览。
Added artifacts/verification/android-purchased-stock.png +0 -0
二进制文件已变更,无法进行逐行预览。
Added artifacts/verification/android-recycled-stock.png +0 -0
二进制文件已变更,无法进行逐行预览。
Modified artifacts/verification/api-checks.txt +1 -1
@@ -1 +1 @@
1 PASS: 24 HTTP checks at 2026-10-01T14:36:51.702Z
1 PASS: 24 HTTP checks at 2026-10-01T15:24:13.659Z
Modified artifacts/verification/apk-signature.txt +9 -0
@@ -4,5 +4,14 @@ Verified using v2 scheme (APK Signature Scheme v2): true
4 4 Verified using v3 scheme (APK Signature Scheme v3): true
5 5 Verified using v3.1 scheme (APK Signature Scheme v3.1): false
6 6 Verified using v4 scheme (APK Signature Scheme v4): false
7 Verified for SourceStamp: false
7 8 Number of signers: 1
9 Signer #1 certificate DN: CN=JiDangJia, OU=Mobile, O=XFEstudio, C=CN
8 10 Signer #1 certificate SHA-256 digest: 4429050070de338a9e51ff19ca828b743e075fbee9b461fed4d8080ec17fcac4
11 Signer #1 certificate SHA-1 digest: 52d38eccb4807a3cd6aa09d60593da7ecdfb469d
12 Signer #1 certificate MD5 digest: 3f3b97f4a3f5d7d1eddd643c277f0e49
13 Signer #1 key algorithm: RSA
14 Signer #1 key size (bits): 3072
15 Signer #1 public key SHA-256 digest: 818bbf0103eeaf1440e7c19ad2a69c97aa1113a8803a13a59192943f27b98a80
16 Signer #1 public key SHA-1 digest: 86b17d22791755bc1391da8a3a1733b9bfc627fe
17 Signer #1 public key MD5 digest: b7949d2477c55a2177806aedb746ceb8
Modified artifacts/verification/quote-live.json +13 -13
@@ -1,5 +1,5 @@
1 1 {
2 "checkedAt": "2026-10-01T14:14:47.362Z",
2 "checkedAt": "2026-10-01T15:29:58.215Z",
3 3 "rows": [
4 4 {
5 5 "key": "gold",
@@ -7,10 +7,10 @@
7 7 "unit": "元/克",
8 8 "digits": 2,
9 9 "code": "JZJ_au_PS",
10 "bid": 896.79,
11 "ask": 899.79,
12 "updown": 0.44,
13 "rate": 0.048,
10 "bid": 899.54,
11 "ask": 902.54,
12 "updown": 3.19,
13 "rate": 0.354,
14 14 "preClose": 899.35
15 15 },
16 16 {
@@ -19,10 +19,10 @@
19 19 "unit": "元/克",
20 20 "digits": 3,
21 21 "code": "JZJ_ag_PS",
22 "bid": 13.656,
23 "ask": 13.756,
24 "updown": 0.001,
25 "rate": 0.007,
22 "bid": 13.631,
23 "ask": 13.731,
24 "updown": -0.024,
25 "rate": -0.174,
26 26 "preClose": 13.755
27 27 },
28 28 {
@@ -31,10 +31,10 @@
31 31 "unit": "元/克",
32 32 "digits": 2,
33 33 "code": "JZJ_pt_PS",
34 "bid": 369.2,
35 "ask": 370.7,
36 "updown": 1,
37 "rate": 0.27,
34 "bid": 368.9,
35 "ask": 370.4,
36 "updown": 0.7,
37 "rate": 0.189,
38 38 "preClose": 369.7
39 39 }
40 40 ]
Modified docs/服务器部署.md +2 -2
@@ -4,7 +4,7 @@
4 4
5 5 ## Windows 本机
6 6
7 解压服务器包,运行 `启动服务器.ps1`。默认端口 5188、回环监听。启动后打开 `data/initial-admin.txt`,在 APP 配置服务器并登录此管理员账户。服务器保持运行,管理员后台在 APP 中。
7 解压服务器包,运行 `启动服务器.ps1`。默认端口 5188、回环监听。启动后打开 `data/initial-admin.txt`,使用当前硬编码服务器地址的 APP 登录此管理员账户。模拟器构建使用 `http://10.0.2.2:5188`。服务器保持运行,管理员后台在 APP 中。
8 8
9 9 首次启动前可以通过环境变量 `JDJ_ADMIN_USERNAME`、`JDJ_ADMIN_PASSWORD` 指定初始管理员。仅在数据库中尚无管理员时生效,密码为 8–128 位。未指定时生成随机密码。
10 10
@@ -15,7 +15,7 @@ dotnet publish src/JiDangJia.Server -c Release -o server
15 15 dotnet server/JiDangJia.Server.dll --urls http://127.0.0.1:5188 --DataDirectory /var/lib/jidangjia
16 16 ```
17 17
18 通过 Nginx 或其他代理配置已获有效证书的 HTTPS 域名,代理到 `127.0.0.1:5188`。APP 登录页填写该 HTTPS 根地址,例如 `https://api.example.com`。模板域名需替换成实际域名。登录和卡密接口使用 Bearer 令牌;管理接口只接受管理员会话,不需要对外提供网页后台。
18 通过 Nginx 或其他代理配置已获有效证书的 HTTPS 域名,代理到 `127.0.0.1:5188`。开发者将 `src/JiDangJia/Services/ServerSettings.cs` 的 `BaseUrl` 修改为实际 HTTPS 根地址后重新编译 APP。该地址无用户配置入口。登录和卡密接口使用 Bearer 令牌;管理接口只接受管理员会话,不需要对外提供网页后台。
19 19
20 20 将进程配置为 Windows 服务或 systemd 服务,数据目录授予运行账户读写权限。只部署一个写入 SQLite 的服务器实例;扩容为多实例时应迁移到共享数据库。
21 21
Added docs/本地联调测试.md +41 -0
@@ -0,0 +1,41 @@
1 # Pixel 7 本地联调记录
2
3 2026-10-01,使用当前运行的 Pixel 7 模拟器(Android 16 / API 36,1080 × 2400)安装本项目签名的 Release APK,连接本机 ASP.NET Core 服务器。通过 ADB 实际操作 MAUI 原生界面,检查控件快照与截图。
4
5 APP 服务器地址固定为 `http://10.0.2.2:5188`,宿主机服务器监听 `http://localhost:5188`。地址唯一配置位置为 `src/JiDangJia/Services/ServerSettings.cs`;登录页、授权页及设置页均无修改入口,客户端不读取旧的 `server_url` 用户设置。地址变更需开发者修改代码并重新编译。
6
7 | 测试内容 | 实际结果 |
8 | --- | --- |
9 | Release 安装和启动 | 安装成功,进入原生登录/工作台界面;修复启动链接错误后可正常冷启动 |
10 | 管理员登录 | 使用本机服务器初始管理员登录;设置页显示「管理员工具 → 进入管理后台」 |
11 | APP 内卡密生成 | 管理员生成一张 1 天授权、7 天兑换有效期的测试卡密 |
12 | CSV 导出 | 打开 Android 系统分享界面,显示卡密 CSV 文件;取消分享,没有发送给外部联系人 |
13 | 普通账户注册 | 在 APP 注册测试账户,进入等待激活页 |
14 | 无效卡密 | 输入无效卡密,显示格式错误,不能进入业务工作台 |
15 | 正常兑换 | 使用管理员生成的卡密激活成功,进入工作台 |
16 | 重复兑换 | 同一账户及设备重复兑换同一卡密,有效期不重复累加 |
17 | 管理员入口隔离 | 普通账户设置页没有「管理员工具」或「进入管理后台」;服务端角色限制另通过 HTTP 测试 |
18 | 首页统计 | 一行横向滑动,顺序为当月利润、库存金额、活当总额、客户总数、今日新开 |
19 | 广告和行情 | 广告自动切换;原生界面持续显示黄金、白银、铂金真实报价,页面切换后行情继续更新;无源报价的黄金 SGE 显示「—」 |
20 | 底部导航 | 点击图标可进入业务、库存、财务;从其他模块返回业务默认回收 |
21 | 不建客户直接回收 | 留空客户档案与姓名,录入测试物品并支付 ¥100,保存成功 |
22 | 新货铺货入库 | 新建供货入库,测试物品成本 ¥200,保存成功 |
23 | 两类库存隔离 | 回收库存仅显示二手物品、成本 ¥100;铺货库存仅显示新货、成本 ¥200 |
24 | 财务与首页汇总 | 流水分别显示回收支出 −¥100、铺货支出 −¥200;首页库存金额为 ¥300 |
25
26 测试过程中修复了 Release 启动错误、行情控件随页面重绘重建、底部绘制图标阻挡点击及 Android 16 状态栏文字对比不足的问题。测试账户及本地测试数据保留用于复查。原始控件快照、账号凭证与完整测试卡密保存在忽略目录 `artifacts/test-data/android-ui`,不包含在仓库和交付包中。
27
28 可公开的测试截图:
29
30 - [工作台与实时行情](../artifacts/verification/android-home.png)
31 - [回收库存](../artifacts/verification/android-recycled-stock.png)
32 - [铺货库存](../artifacts/verification/android-purchased-stock.png)
33
34 自动检查记录:
35
36 - [75 项业务、备份和授权检查](../artifacts/verification/business-checks.txt)
37 - [24 项实际 HTTP 权限检查](../artifacts/verification/api-checks.txt)
38 - [真实行情连接记录](../artifacts/verification/quote-live.json)
39 - [APK 签名验证](../artifacts/verification/apk-signature.txt)
40
41 本次模拟器联调未验证实体相机、实际打印机、24 小时离线等待及完整设备兼容性。继续验收步骤见 [设备验收](验收步骤.md)。
Modified docs/验收步骤.md +2 -2
@@ -1,8 +1,8 @@
1 1 # 寄当家设备验收
2 2
3 业务、授权和 HTTP 接口已自动验证。以下步骤用于真机验收 MAUI 界面、相机、文件选取、后台恢复、系统分享和打印。
3 业务、授权和 HTTP 接口已自动验证,Pixel 7 / API 36 已完成本地联调,结果见 [本地联调测试](本地联调测试.md)。以下完整步骤用于继续真机验收 MAUI 界面、相机、文件选取、后台恢复、系统分享和打印。
4 4
5 1. 启动服务器。通过 USB 使用 `adb reverse tcp:5188 tcp:5188`,或部署 HTTPS 服务。安装 APK,配置服务器地址。
5 1. 启动本地服务器,监听 5188。Pixel 7 模拟器使用当前 APK 中固定的 `http://10.0.2.2:5188`,安装后直接登录;确认登录及设置页面没有地址修改入口。其他设备或部署地址由开发者修改 `ServerSettings.BaseUrl` 后重新编译。
6 6 2. 用 `data/initial-admin.txt` 中的账户登录,在设置中确认出现管理员入口。生成两张 30 天卡密,导出 CSV 并复制其中一张。
7 7 3. 退出管理员,注册普通账户。确认普通账户看不到管理员入口。兑换第一张卡密,确认授权有效,再输入同一卡密,确认有效期未重复累加。
8 8 4. 在首页确认配色、五项数据顺序、横向滑动、广告轮播、实时贵金属价格、客户管理按钮和经营图表;在设置中为广告添加图片,回首页查看。
Modified src/JiDangJia.Server/ServerStore.cs +1 -1
@@ -28,7 +28,7 @@ public sealed class ServerStore
28 28 var password=Environment.GetEnvironmentVariable("JDJ_ADMIN_PASSWORD")??"Jdj!"+Convert.ToHexString(RandomNumberGenerator.GetBytes(10));
29 29 ValidatePassword(password);
30 30 Exec(c,"INSERT INTO Accounts(id,username,name,passwordHash,role,createdAt) VALUES(@p0,@p1,@p2,@p3,'admin',@p4)",Guid.NewGuid().ToString("N"),username,"系统管理员",PasswordHash(password),Now);
31 File.WriteAllText(Path.Combine(dataDirectory,"initial-admin.txt"),$"寄当家管理员初始账户\n账号:{username}\n密码:{password}\n在 APP 登录页配置此服务器地址,使用以上账户登录,然后进入:设置 → 管理员工具 → 进入管理后台。\n首次登录后请修改密码。\n",Encoding.UTF8);
31 File.WriteAllText(Path.Combine(dataDirectory,"initial-admin.txt"),$"寄当家管理员初始账户\n账号:{username}\n密码:{password}\n服务器地址固定在 APP 源码中。使用以上账户登录,然后进入:设置 → 管理员工具 → 进入管理后台。\n首次登录后请修改密码。\n",Encoding.UTF8);
32 32 }
33 33 }
34 34 public static string Now=>DateTime.UtcNow.ToString("O");
Modified src/JiDangJia/JiDangJia.csproj +1 -0
@@ -14,6 +14,7 @@
14 14 <SupportedOSPlatformVersion>23.0</SupportedOSPlatformVersion>
15 15 <AndroidPackageFormats>apk</AndroidPackageFormats>
16 16 <AndroidEnableProfiledAot>false</AndroidEnableProfiledAot>
17 <AndroidEnableMarshalMethods>false</AndroidEnableMarshalMethods>
17 18 <EmbedAssembliesIntoApk>true</EmbedAssembliesIntoApk>
18 19 <PublishTrimmed>false</PublishTrimmed>
19 20 <RunAOTCompilation>false</RunAOTCompilation>
Modified src/JiDangJia/MainPage.xaml.cs +3 -2
@@ -61,11 +61,12 @@ public partial class MainPage : ContentPage
61 61 private Task NewOrder() => businessTab==4?Switch(2):Push(FormPage.Order(service,businessTab switch {0=>"RECYCLE",1=>"PAWN",2=>"CONSIGN",3=>"RENT",_=>"OTHER"}));
62 62 private void Render()
63 63 {
64 Root.Children.Clear();quoteLabels.Clear();ads=null;quoteFoot=null;
64 foreach(var child in Root.Children.Where(child=>!ReferenceEquals(child,feed)).ToList())Root.Children.Remove(child);
65 quoteLabels.Clear();ads=null;quoteFoot=null;
65 66 View body=tab switch {0=>Dashboard(),1=>Business(),2=>Stock(),3=>Finance(),_=>Community()};
66 67 var refresh=new RefreshView { Content=new ScrollView { Content=body },RefreshColor=Ui.Bronze };
67 68 refresh.Refreshing+=(_,_)=> {Render();feed.SendRawMessage("refresh");refresh.IsRefreshing=false;};
68 Root.Add(refresh); Root.Add(NavigationBar(),0,1); Root.Add(feed);
69 Root.Add(refresh); Root.Add(NavigationBar(),0,1); if(!Root.Children.Contains(feed))Root.Add(feed);
69 70 }
70 71 private View NavigationBar()
71 72 {
Modified src/JiDangJia/Platforms/Android/MainActivity.cs +22 -1
@@ -18,7 +18,28 @@ public class MainActivity : MauiAppCompatActivity
18 18 if(Window is { } window)
19 19 {
20 20 var insets=AndroidX.Core.View.WindowCompat.GetInsetsController(window,window.DecorView);
21 if(insets is not null){insets.AppearanceLightStatusBars=OperatingSystem.IsAndroidVersionAtLeast(35);insets.AppearanceLightNavigationBars=true;}
21 if(insets is not null){insets.AppearanceLightStatusBars=false;insets.AppearanceLightNavigationBars=true;}
22 if(OperatingSystem.IsAndroidVersionAtLeast(35)&&window.DecorView is Android.Views.ViewGroup decor)
23 {
24 // Android 15+ 状态栏透明,直接在系统栏区域绘制暖黑背景,保持白色图标清晰。
25 var protection=new Android.Views.View(this){Clickable=false,Focusable=false,ImportantForAccessibility=Android.Views.ImportantForAccessibility.No};
26 protection.SetBackgroundColor(Android.Graphics.Color.ParseColor("#2B211A"));
27 decor.AddView(protection,new Android.Widget.FrameLayout.LayoutParams(Android.Views.ViewGroup.LayoutParams.MatchParent,0,Android.Views.GravityFlags.Top));
28 AndroidX.Core.View.ViewCompat.SetOnApplyWindowInsetsListener(protection,new StatusBarInsets());
29 protection.Post(()=>AndroidX.Core.View.ViewCompat.RequestApplyInsets(protection));
30 }
31 }
32 }
33
34 private sealed class StatusBarInsets : Java.Lang.Object, AndroidX.Core.View.IOnApplyWindowInsetsListener
35 {
36 public AndroidX.Core.View.WindowInsetsCompat? OnApplyWindowInsets(Android.Views.View? view,AndroidX.Core.View.WindowInsetsCompat? insets)
37 {
38 if(view is null||insets is null)return insets;
39 var height=insets.GetInsets(AndroidX.Core.View.WindowInsetsCompat.Type.StatusBars()|AndroidX.Core.View.WindowInsetsCompat.Type.DisplayCutout())?.Top??0;
40 if(view.LayoutParameters is { } layout&&layout.Height!=height){layout.Height=height;view.LayoutParameters=layout;}
41 view.BringToFront();
42 return insets;
22 43 }
23 44 }
24 45 }
Modified src/JiDangJia/Services/AuthClient.cs +3 -8
Added src/JiDangJia/Services/ServerSettings.cs +7 -0
Modified src/JiDangJia/Views/AccountPages.cs +2 -2
Modified src/JiDangJia/Views/Ui.cs +2 -1
Added tools/android-ui.cjs +35 -0
Modified tools/package-delivery.py +1 -1