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

STHApplication

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

公开
关注 0 Fork 0 Star 0
UTF-8
using System.Text.Json;
using System.Text.RegularExpressions;
using JiDangJia.Core;
using JiDangJia.Views;

namespace JiDangJia.Services;

public sealed record OrderCameraDraft(string AccountId,DbRow Header,DbRow Terms,List<DbRow> Items,string Field,int ItemIndex=-1,string CapturePath="",string DraftId="");

// Keep an unfinished pawn form until it is saved or explicitly dismissed.
// A camera result and a renewed login can arrive in either order.
public static class OrderCameraRecovery
{
    private static bool restoring;
    private static readonly HashSet<string> capturing=[];
    public static bool Capturing(string account)=>capturing.Contains(account);
    private static OrderCameraDraft? ReadDraft(string json)
    {
        var draft=JsonSerializer.Deserialize<OrderCameraDraft>(json);if(draft is null)return null;
        // Dictionary<object> deserialization creates JsonElement values; normalize them
        // before the restored form passes hidden/default fields to SQLite.
        return draft with{Header=DbRow.FromJson(JsonSerializer.SerializeToElement(draft.Header)),Terms=DbRow.FromJson(JsonSerializer.SerializeToElement(draft.Terms)),Items=draft.Items.Select(row=>DbRow.FromJson(JsonSerializer.SerializeToElement(row))).ToList()};
    }
    private static string DraftPath(string account)
    {
        if(!Regex.IsMatch(account,"^[a-f0-9]{32}$"))throw new InvalidOperationException("登录账户无效");
        var directory=Path.Combine(FileSystem.AppDataDirectory,"accounts",account);Directory.CreateDirectory(directory);return Path.Combine(directory,"camera-draft.json");
    }
    public static bool OwnsPhoto(string account,string path)
    {
        if(!Regex.IsMatch(account,"^[a-f0-9]{32}$")||string.IsNullOrWhiteSpace(path)||!Path.IsPathRooted(path))return false;
        var directory=Path.GetFullPath(Path.Combine(FileSystem.CacheDirectory,"accounts",account))+Path.DirectorySeparatorChar;
        return Path.GetFullPath(path).StartsWith(directory,StringComparison.Ordinal)&&Regex.IsMatch(Path.GetFileName(path),"^camera-[a-f0-9]{32}\\.jpg$");
    }
    public static string Save(OrderCameraDraft draft)
    {
        AuthClient.Current.RequireLicense();if(draft.AccountId!=AuthClient.Current.AccountId)throw new InvalidOperationException("账户已切换");
        return Store(draft);
    }
    private static string Store(OrderCameraDraft draft)
    {
        var path=DraftPath(draft.AccountId);File.WriteAllText(path+".tmp",JsonSerializer.Serialize(draft));File.Move(path+".tmp",path,true);return path;
    }
    public static void Discard(string account,string draftId)
    {
        var path=DraftPath(account);if(!File.Exists(path))return;
        var saved=ReadDraft(File.ReadAllText(path));if(saved?.DraftId==draftId)Complete(account,saved.CapturePath);
    }
    private static OrderCameraDraft Attach(OrderCameraDraft draft,string photo)
    {
        var row=draft.ItemIndex>=0&&draft.ItemIndex<draft.Items.Count?draft.Items[draft.ItemIndex]:draft.ItemIndex==-1?draft.Terms:null;
        if(row is not null){var photos=MediaFiles.Parse(row.Text(draft.Field));photos.Add(photo);row[draft.Field]=JsonSerializer.Serialize(photos);}
        return draft with{CapturePath="",Field="",ItemIndex=-1};
    }
    private static bool StoreCaptureResult(OrderCameraDraft original,OrderCameraDraft result)
    {
        // Do not resurrect a form removed by a successful save, dismissal or data clear.
        var path=DraftPath(original.AccountId);if(!File.Exists(path))return false;
        var saved=ReadDraft(File.ReadAllText(path));if(saved?.DraftId!=original.DraftId||saved.CapturePath!=original.CapturePath)return false;
        Store(result);return true;
    }
    public static void Complete(string account,string photo)
    {
        if(OwnsPhoto(account,photo))File.Delete(photo);
        var path=DraftPath(account);File.Delete(path);File.Delete(path+".result");File.Delete(path+".tmp");
    }
    public static void RecordResult(string account,string photo,bool accepted)
    {
        if(!OwnsPhoto(account,photo))return;
        var path=DraftPath(account);if(!File.Exists(path))return;
        var draft=ReadDraft(File.ReadAllText(path));
        if(draft?.AccountId!=account||draft.CapturePath!=photo)return;
        File.WriteAllText(path+".result",accepted?"OK":"CANCEL");
    }
    public static async Task Restore(MainPage root,BusinessService service)
    {
        if(restoring||Capturing(AuthClient.Current.AccountId))return;string account=AuthClient.Current.AccountId,path=DraftPath(account);if(!File.Exists(path))return;restoring=true;
        try
        {
            var draft=ReadDraft(await File.ReadAllTextAsync(path));
            if(draft is null||draft.AccountId!=account||draft.CapturePath.Length>0&&!OwnsPhoto(account,draft.CapturePath))return;
            if(draft.CapturePath.Length>0&&File.Exists(path+".result")&&await File.ReadAllTextAsync(path+".result")=="OK"&&File.Exists(draft.CapturePath)&&new FileInfo(draft.CapturePath).Length>0)
            {
                var captured=draft.CapturePath;var photo=await MediaFiles.ReadPhoto(new FileResult(captured,"image/jpeg"),false);
                draft=Attach(draft,photo);Store(draft);File.Delete(captured);File.Delete(path+".result");
            }
            if(account!=AuthClient.Current.AccountId||AuthClient.Current.Locked||(Application.Current?.Windows.FirstOrDefault()?.Page as NavigationPage)?.RootPage!=root)return;
            AuthClient.Current.RequireLicense();await root.Navigation.PushAsync(FormPage.Order(service,"PAWN",draft:draft),false);
        }
        catch(Exception ex){await root.DisplayAlertAsync("拍照页面恢复",ex is ArgumentException or IOException?"未能恢复拍照前的表单,请重新打开当单。":ex.Message,"知道了");}
        finally{restoring=false;}
    }
    public static async Task<string?> Capture(OrderCameraDraft draft)
    {
#if ANDROID
        if(Capturing(draft.AccountId))throw new InvalidOperationException("相机正在使用中");
        var photo=AccountDataClear.CacheFile("camera-"+Guid.NewGuid().ToString("N")+".jpg");draft=draft with{CapturePath=photo};Save(draft);capturing.Add(draft.AccountId);
        try
        {
            using var photoScope=new ExternalPhotoScope();
            if(await Permissions.RequestAsync<Permissions.Camera>()!=PermissionStatus.Granted)throw new InvalidOperationException("请允许使用相机后重试");
            var result=await OrderPhotoActivity.Capture(draft.AccountId,photo);
            photoScope.Dispose();
            if(result is null){StoreCaptureResult(draft,draft with{CapturePath="",Field="",ItemIndex=-1});File.Delete(photo);File.Delete(DraftPath(draft.AccountId)+".result");return null;}
            // Persist the image with the original account's form before consuming the camera file.
            // The session may have been invalidated while the external activity was open.
            var image=await MediaFiles.ReadPhoto(result,false);var retained=StoreCaptureResult(draft,Attach(draft,image));File.Delete(photo);File.Delete(DraftPath(draft.AccountId)+".result");return retained?image:null;
        }
        finally
        {
            capturing.Remove(draft.AccountId);
            if(AuthClient.Current.AccountId==draft.AccountId&&!AuthClient.Current.Locked&&Application.Current?.Windows.FirstOrDefault()?.Page is NavigationPage navigation&&navigation.RootPage is MainPage root&&!navigation.Navigation.NavigationStack.OfType<FormPage>().Any())_ = root.RestoreOrderDraft();
        }
#else
        return await MediaFiles.Pick(true);
#endif
    }
}