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

STHApplication

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

公开
关注 0 Fork 0 Star 0
UTF-8
using JiDangJia.Contracts;
using JiDangJia.Core;
using JiDangJia.Services;
using System.Globalization;
using System.Text.Json;

namespace JiDangJia.Views;

public static class PurchaseUi
{
    public static string Price(int cents)=>(cents/100m).ToString("0.##",System.Globalization.CultureInfo.InvariantCulture);
    public static View Plan(CardPlan plan,Func<Task>? select=null)
    {
        var discounted=plan.OriginalPriceCents>plan.PriceCents;
        var original=Ui.Text("原价 ¥"+Price(plan.OriginalPriceCents),12,Ui.Muted);original.TextDecorations=TextDecorations.Strikethrough;original.IsVisible=discounted;
        var badge=Ui.Pill(PurchaseCatalog.ExperienceLabel);badge.IsVisible=discounted;
        var content=Ui.Stack(Ui.Spread(Ui.Text(plan.Name,19,bold:true),badge),Ui.Row(Ui.Text("¥"+Price(plan.PriceCents),30,Ui.Bronze,true),original),Ui.Text(plan.Permanent?"激活后永久使用":"激活后使用 365 天",12,Ui.Muted));
        if(select is not null)content.Add(Ui.Button("购买"+plan.Name,select));return Ui.Card(content,18,background:Ui.Paper);
    }
    public static PurchasePanel Panel(INavigation navigation,Func<string,Task>? useCode=null)=>new(navigation,useCode);
    public static string State(string status)=>status switch{"PENDING"=>"待付款","EXPIRED"=>"订单已过期","CLOSED"=>"订单已关闭","PAID"=>"付款成功","REFUND_REVIEW"=>"退款核对中",_=>"待查询"};
}

public sealed class PurchasePanel : VerticalStackLayout
{
    private readonly VerticalStackLayout plans=Ui.Stack();
    private readonly Label status=Ui.Text("正在读取卡密价格…",12,Ui.Muted);
    private readonly INavigation navigation;
    private readonly Func<string,Task>? useCode;
    private bool loading;
    private long revision=-1;
    public PurchasePanel(INavigation navigation,Func<string,Task>? useCode)
    {
        this.navigation=navigation;this.useCode=useCode;Spacing=12;
        Add(Ui.Section("购买卡密"));Add(plans);Add(status);Add(Ui.Button("刷新卡密价格",Refresh,false));
        Add(Ui.Button("扫码联系客服购买",()=>navigation.PushAsync(new PurchaseSupportPage()),false));
        Add(Ui.Button("需要发票?联系客服申请",()=>navigation.PushAsync(new PurchaseSupportPage(true)),false));
        Add(Ui.Button("我的购买订单",()=>navigation.PushAsync(new PurchaseHistoryPage(useCode)),false));
    }
    public async Task Refresh()
    {
        if(loading)return;loading=true;
        try
        {
            var offer=await PurchaseClient.Current.Offer();
            if(revision!=offer.Revision||plans.Count==0)
            {
                plans.Clear();foreach(var plan in offer.Plans)plans.Add(PurchaseUi.Plan(plan,()=>navigation.PushAsync(new PurchasePage(plan.Id,useCode))));revision=offer.Revision;
            }
            status.Text="价格已同步 · "+DateTime.Now.ToString("HH:mm");
        }
        catch(Exception ex){status.Text=plans.Count==0?ex.Message:"价格暂未更新,以上为上次同步价格。请刷新或联系客服确认。";}
        finally{loading=false;}
    }
}

public sealed class PurchaseHistoryPage : ContentPage
{
    private readonly VerticalStackLayout list=Ui.Stack();
    private readonly Func<string,Task>? useCode;
    public PurchaseHistoryPage(Func<string,Task>? useCode=null)
    {
        this.useCode=useCode;Title="我的购买订单";var body=Ui.Stack(Ui.Text("订单凭证保存在此设备。再次购买后,仍可在这里查询之前的订单与卡密。",12,Ui.Muted),list);body.Padding=20;Content=new ScrollView{Content=body};
    }
    protected override async void OnAppearing()
    {
        base.OnAppearing();list.Clear();
        try
        {
            var receipts=await PurchaseClient.Current.Receipts();if(receipts.Count==0)list.Add(Ui.Card(Ui.Text("暂无支付宝购买订单",14,Ui.Muted)));
            foreach(var receipt in receipts)
            {
                var result=Ui.Stack();var content=Ui.Stack(Ui.Text(PurchaseCatalog.Plan(receipt.PlanId).Name,18,bold:true),Ui.Text(receipt.OrderId,11,Ui.Muted),result);
                content.Add(Ui.Button("查询 / 领取卡密",async()=>
                {
                    var order=await PurchaseClient.Current.Past(receipt.OrderId);result.Clear();result.Add(Ui.Text(PurchaseUi.State(order.Status),14,Ui.Bronze,true));
                    if(order.Status=="PAID"&&order.CardCode.Length>0){result.Add(Ui.Text(order.CardCode,15));result.Add(Ui.Button("复制卡密",()=>Clipboard.Default.SetTextAsync(order.CardCode),false));if(useCode is not null)result.Add(Ui.Button("填入卡密并返回",()=>useCode(order.CardCode)));}
                },false));list.Add(Ui.Card(content));
            }
        }
        catch(Exception ex){list.Add(Ui.Text(ex.Message,12,Ui.Red));}
    }
}

public sealed class PurchasePage : ContentPage
{
    private CardPlan plan;
    private readonly Func<string,Task>? useCode;
    private readonly Label status=Ui.Text("正在连接购买服务…",12,Ui.Muted);
    private readonly VerticalStackLayout result=Ui.Stack();
    private readonly VerticalStackLayout priceCard=Ui.Stack(Ui.Text("正在读取卡密价格…",14,Ui.Muted));
    private readonly Button pay;
    private PurchaseOffer? offer;
    private PurchaseOrder? order;
    private bool loading;
    public PurchasePage(string planId,Func<string,Task>? useCode=null)
    {
        plan=PurchaseCatalog.Plan(planId);this.useCode=useCode;Title="购买"+plan.Name;
        pay=Ui.Button("支付宝支付",Pay);pay.IsEnabled=false;
        var support=Ui.Card(Ui.Stack(Ui.Section("客服购买与发票"),Ui.Text("也可扫描抖音二维码联系客服购买。支持申请发票,请添加客服提供订单与开票信息。",13,Ui.Muted),Ui.Button("扫码联系客服购买",()=>Navigation.PushAsync(new PurchaseSupportPage()),false),Ui.Button("申请发票",()=>Navigation.PushAsync(new PurchaseSupportPage(true)),false)));
        var body=Ui.Stack(priceCard,status,pay,Ui.Button("刷新价格 / 查询付款结果",Refresh,false),result,support);
        body.Padding=20;Content=new ScrollView{Content=body};
    }
    protected override async void OnAppearing(){base.OnAppearing();try{await Refresh();}catch(Exception ex){status.Text=ex.Message;}}
    private async Task Refresh()
    {
        if(loading)return;loading=true;pay.IsEnabled=false;
        try{await LoadOffer();order=await PurchaseClient.Current.Existing(plan.Id);Render();}
        finally{loading=false;pay.IsEnabled=offer is not null&&order?.Status!="REFUND_REVIEW";}
    }
    private async Task LoadOffer()
    {
        var fresh=await PurchaseClient.Current.Offer();plan=fresh.Plans.Single(p=>p.Id==plan.Id);offer=fresh;status.Text=fresh.Message;
        priceCard.Clear();priceCard.Add(PurchaseUi.Plan(plan));
    }
    private int PaymentAmount()=>order?.Status=="PENDING"?order.AmountCents:plan.PriceCents;
    private void Render()
    {
        pay.Text=order?.Status=="PAID"?"再购买一张"+plan.Name:order?.Status is "EXPIRED" or "CLOSED"?"重新购买 · 支付宝 ¥"+PurchaseUi.Price(plan.PriceCents):"支付宝支付 ¥"+PurchaseUi.Price(PaymentAmount());
        result.Clear();if(order is null)return;
        var details=Ui.Stack(Ui.Section(PurchaseUi.State(order.Status)),Ui.Text("订单号:"+order.Id,11,Ui.Muted),Ui.Text("实付金额:¥"+PurchaseUi.Price(order.AmountCents),13));
        // Keep unpaid amounts clearly labelled; only a verified server result displays a card.
        if(order.Status!="PAID")((Label)details.Children[2]).Text="订单金额:¥"+PurchaseUi.Price(order.AmountCents);
        if(order.Status=="PENDING")details.Add(Ui.Text("付款后返回这里查询结果;如已付款请勿重复支付。",12,Ui.Muted));
        if(order.Status=="PENDING"&&order.AmountCents!=plan.PriceCents)details.Add(Ui.Text("此订单保留创建时售价;新订单当前售价为 ¥"+PurchaseUi.Price(plan.PriceCents)+"。",12,Ui.Muted));
        if(order.Status=="PAID"&&order.CardCode.Length>0)
        {
            var cardCode=order.CardCode;details.Add(Ui.Text(cardCode,15,Ui.Bronze,true));
            details.Add(Ui.Button("复制卡密",()=>Clipboard.Default.SetTextAsync(cardCode),false));
            if(useCode is not null)details.Add(Ui.Button("填入卡密并返回",()=>useCode(cardCode)));
            details.Add(Ui.Text("请妥善保存卡密;兑换后绑定账户和设备。",12,Ui.Muted));
        }
        result.Add(Ui.Card(details));
    }
    private async Task Pay()
    {
        var displayedAmount=PaymentAmount();await LoadOffer();
        order=await PurchaseClient.Current.Existing(plan.Id);Render();
        if(!offer!.AlipayEnabled){await Navigation.PushAsync(new PurchaseSupportPage());return;}
        var replace=order?.Status is "EXPIRED" or "CLOSED" or "PAID";
        if(order?.Status=="REFUND_REVIEW")throw new InvalidOperationException("此订单正在退款核对,请联系客服");
        if(order?.Status=="PAID")
        {
            if(!await DisplayAlertAsync("已有已付款订单","已购买的卡密仍可在此页领取。确定以 ¥"+PurchaseUi.Price(plan.PriceCents)+" 再购买一张?","再购买","取消"))return;
            displayedAmount=plan.PriceCents;
        }
        order=await PurchaseClient.Current.Create(plan.Id,replace);Render();if(order.Status=="PAID")return;
        if(order.AmountCents!=displayedAmount&&!await DisplayAlertAsync("价格已更新","此订单付款金额为 ¥"+PurchaseUi.Price(order.AmountCents)+",与此前显示的价格不同。请确认后继续付款。","确认金额并支付","取消"))return;
        var checkout=await PurchaseClient.Current.Checkout(plan.Id,order.Id);var uri=new Uri(checkout.Url);
        if(uri.Scheme!="https"||uri.Host!="openapi.alipay.com"||uri.AbsolutePath!="/gateway.do")throw new InvalidOperationException("支付地址无效,请联系客服");
        await Browser.Default.OpenAsync(uri,BrowserLaunchMode.External);status.Text="已打开支付宝付款页,付款后返回查询结果。";
    }
}

public sealed class PurchaseSupportPage : ContentPage
{
    private readonly Image qr=new(){Aspect=Aspect.AspectFit,HeightRequest=345};
    private readonly Label message=Ui.Text("",12,Ui.Muted);
    private readonly VerticalStackLayout body=Ui.Stack();
    private readonly bool invoice;
    private byte[]? bytes;
    public PurchaseSupportPage(bool invoice=false)
    {
        this.invoice=invoice;Title=invoice?"申请发票":"客服购买";body.Padding=20;Content=new ScrollView{Content=body};SemanticProperties.SetDescription(qr,"客服二维码");
    }
    protected override async void OnAppearing()
    {
        base.OnAppearing();message.Text="";try{await ContentClient.Refresh(true);}catch{message.Text="暂时无法同步,显示上次配置。";}
        var entry=ContentClient.Current?.Support?.FirstOrDefault(e=>e.Id==(invoice?"invoice-contact":"purchase-contact"));bool custom=!string.IsNullOrEmpty(entry?.QrImage);
        body.Clear();body.Add(Ui.Section(entry?.Title??Title));body.Add(Ui.Text(entry?.Description??(invoice?"请添加客服,提供订单、发票抬头及开票信息。":"扫描二维码联系客服购买卡密。"),13,Ui.Muted));
        try
        {
            if(custom)bytes=Convert.FromBase64String(entry!.QrImage[(entry.QrImage.IndexOf(',')+1)..]);
            else{using var source=await FileSystem.OpenAppPackageFileAsync("purchase-support-douyin.png");using var buffer=new MemoryStream();await source.CopyToAsync(buffer);bytes=buffer.ToArray();}
            qr.Source=ImageSource.FromStream(()=>new MemoryStream(bytes!));body.Add(Ui.Card(qr,16,new Thickness(10)));
        }
        catch{bytes=null;message.Text="二维码暂不可用,请通过客服与反馈联系。";}
        if(!custom){body.Add(Ui.Text(PurchaseCatalog.SupportName+"\n抖音号:"+PurchaseCatalog.DouyinId,16,Ui.Bronze,true));body.Add(Ui.Button("复制抖音号",()=>Clipboard.Default.SetTextAsync(PurchaseCatalog.DouyinId),false));}
        if(entry?.Phone.Length>0)body.Add(Ui.Button("复制客服电话:"+entry.Phone,()=>Clipboard.Default.SetTextAsync(entry.Phone),false));
        if(entry?.Link.Length>0)body.Add(Ui.Button("联系客服",()=>Browser.OpenAsync(entry.Link,BrowserLaunchMode.SystemPreferred),false));
        body.Add(Ui.Button("分享 / 保存客服二维码",ShareQr,false));body.Add(message);body.Add(Ui.Button("客服与反馈",()=>Navigation.PushAsync(new SupportPage()),false));
    }
    private async Task ShareQr()
    {
        if(bytes is null)throw new InvalidOperationException("二维码尚未加载,请稍后再试");
        var path=Path.Combine(FileSystem.CacheDirectory,AppProduct.Name+"-抖音客服.png");await File.WriteAllBytesAsync(path,bytes);await Share.Default.RequestAsync(new ShareFileRequest(AppProduct.Name+"抖音客服二维码",new ShareFile(path,"image/png")));
    }
}

public sealed class PurchaseAdminPage : ContentPage
{
    private readonly VerticalStackLayout list=Ui.Stack();
    private readonly Entry search=new(){Placeholder="订单号 / 支付宝交易号",MaxLength=64};
    public PurchaseAdminPage()
    {
        if(!AuthClient.Current.IsAdmin)throw new InvalidOperationException("需要管理员权限");Title="卡密购买订单";
        var body=Ui.Stack(Ui.Card(search,12,new Thickness(12,0)),Ui.Button("查询订单",Load,false),Ui.Text("展示最近 100 条订单。退款后请核对卡密与授权;已兑换授权在管理员授权列表处理。",12,Ui.Muted),list);body.Padding=18;Content=new ScrollView{Content=body};
    }
    protected override async void OnAppearing(){base.OnAppearing();try{await Load();}catch(Exception ex){await DisplayAlertAsync("无法加载",ex.Message,"知道了");}}
    private async Task Load()
    {
        using var d=await AuthClient.Current.AdminRequest("/purchase?q="+Uri.EscapeDataString(search.Text??""));list.Clear();list.Add(Ui.Text(d.RootElement.GetProperty("message").GetString()??"",12,Ui.Bronze));
        foreach(var item in d.RootElement.GetProperty("orders").EnumerateArray())
        {
            var r=DbRow.FromJson(item);var state=r.Text("status");if(state=="PENDING"&&r.Date("expiresAt")<DateTime.UtcNow)state="EXPIRED";
            list.Add(Ui.Card(Ui.Stack(Ui.Spread(Ui.Text(PurchaseCatalog.Plan(r.Text("planId")).Name,17,bold:true),Ui.Pill(PurchaseUi.State(state))),Ui.Text(r.Id,11,Ui.Muted),Ui.Text("¥"+PurchaseUi.Price(r.Int("amountCents")),22,Ui.Bronze,true),Ui.Text("支付宝交易号:"+r.Text("alipayTradeNo","—"),11,Ui.Muted),Ui.Text("卡密:"+r.Text("maskedCode","—"),11,Ui.Muted),Ui.Text("创建时间:"+r.Date("createdAt").ToLocalTime().ToString("yyyy-MM-dd HH:mm"),11,Ui.Muted))));
        }
    }
}

public sealed class PurchasePricesAdminPage : ContentPage
{
    private static readonly JsonSerializerOptions json=new(JsonSerializerDefaults.Web);
    private readonly Dictionary<string,(Entry Original,Entry Actual)> fields=new();
    private readonly Button save;
    private readonly Label status=Ui.Text("正在加载价格…",12,Ui.Muted);
    private PurchasePrices? prices;
    public PurchasePricesAdminPage()
    {
        if(!AuthClient.Current.IsAdmin)throw new InvalidOperationException("需要管理员权限");Title="卡密价格管理";
        var body=Ui.Stack(Ui.Text("保存后发布到登录页与购买页。金额单位为元,最多两位小数。已创建订单保留原金额,卡密有效期保持不变。",13,Ui.Muted));
        foreach(var plan in PurchaseCatalog.Plans)
        {
            var original=new Entry{Keyboard=Keyboard.Numeric,MaxLength=12,Placeholder=plan.Name+"原价"};
            var actual=new Entry{Keyboard=Keyboard.Numeric,MaxLength=12,Placeholder=plan.Name+"实际售价"};fields.Add(plan.Id,(original,actual));
            SemanticProperties.SetDescription(original,plan.Name+"原价");SemanticProperties.SetDescription(actual,plan.Name+"实际售价");
            body.Add(Ui.Card(Ui.Stack(Ui.Section(plan.Name),Ui.Text("原价(元)",13,Ui.Muted),Ui.Card(original,12,new Thickness(12,0),Ui.Paper),Ui.Text("优惠实际价(元)",13,Ui.Muted),Ui.Card(actual,12,new Thickness(12,0),Ui.Paper))));
        }
        save=Ui.Button("保存并发布价格",Save);save.IsEnabled=false;body.Add(status);body.Add(save);body.Add(Ui.Button("重新加载价格",Load,false));body.Padding=18;Content=new ScrollView{Content=body};
    }
    protected override async void OnAppearing(){base.OnAppearing();try{await Load();}catch(Exception ex){status.Text=ex.Message;}}
    private async Task Load()
    {
        using var data=await AuthClient.Current.AdminRequest("/purchase/prices");
        prices=data.RootElement.Deserialize<PurchasePrices>(json)??throw new InvalidOperationException("价格数据无效");
        foreach(var plan in prices.Plans){var pair=fields[plan.Id];pair.Original.Text=PurchaseUi.Price(plan.OriginalPriceCents);pair.Actual.Text=PurchaseUi.Price(plan.PriceCents);}
        status.Text="已加载当前价格";save.IsEnabled=true;
    }
    private static int Cents(Entry input,string label)
    {
        if(!decimal.TryParse(input.Text?.Trim(),NumberStyles.AllowDecimalPoint,CultureInfo.InvariantCulture,out var value)||value<0.01m||value*100>PurchaseCatalog.MaximumPriceCents||value*100!=decimal.Truncate(value*100))throw new ArgumentException(label+"须为 0.01 至 1000000 元,最多两位小数");
        return (int)(value*100);
    }
    private async Task Save()
    {
        if(prices is null)throw new InvalidOperationException("请先加载价格");
        var values=prices.Plans.Select(plan=>{var pair=fields[plan.Id];return new CardPrice(plan.Id,Cents(pair.Actual,plan.Name+"售价"),Cents(pair.Original,plan.Name+"原价"));}).ToList();
        if(values.Any(p=>p.OriginalPriceCents<p.PriceCents))throw new ArgumentException("原价不能低于实际售价");
        using var data=await AuthClient.Current.AdminRequest("/purchase/prices",new UpdatePurchasePricesRequest(prices.Revision,values));
        prices=data.RootElement.Deserialize<PurchasePrices>(json)??throw new InvalidOperationException("价格数据无效");status.Text="价格已发布";
        await DisplayAlertAsync("价格已发布","登录页和购买页重新加载时会同步新价格;登录页每分钟自动刷新。已有订单金额保持不变。","完成");
    }
}