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

STHApplication

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

公开
关注 0 Fork 0 Star 0
UTF-8
using JiDangJia.Contracts;
using Microsoft.Data.Sqlite;
using System.Text.Json;
using static JiDangJia.Server.ServerStore;

namespace JiDangJia.Server;

public sealed class ContentStore
{
    private readonly ServerStore store;
    public ContentStore(ServerStore store)
    {
        this.store=store;
        store.Write(c=>
        {
            Exec(c,"CREATE TABLE IF NOT EXISTS AppContent(id INTEGER PRIMARY KEY CHECK(id=1),revision INTEGER NOT NULL,ads TEXT NOT NULL,communityUrl TEXT NOT NULL,updatedAt TEXT NOT NULL)");
            if(!Rows(c,"PRAGMA table_info(AppContent)").Any(r=>Text(r,"name")=="support"))
            {
                Exec(c,"ALTER TABLE AppContent ADD COLUMN support TEXT NOT NULL DEFAULT '[]'");
                Exec(c,"UPDATE AppContent SET support=@p0",JsonSerializer.Serialize(SupportCatalog.Defaults));
            }
            if(!Rows(c,"PRAGMA table_info(AppContent)").Any(r=>Text(r,"name")=="about"))Exec(c,"ALTER TABLE AppContent ADD COLUMN about TEXT NOT NULL DEFAULT ''");
#if COMMUNITY_APP
            var ads=new List<Advertisement>();
#else
            var ads=new List<Advertisement>{new("welcome","让每一笔生意\n清楚,有据","寄当家 · 您的门店经营助手","","",true,0),new("assets","安心管理资产\n从容经营每一天","客户 · 业务 · 库存 · 财务","","",true,1)};
#endif
            Exec(c,"INSERT OR IGNORE INTO AppContent(id,revision,ads,communityUrl,updatedAt,support) VALUES(1,1,@p0,'',@p1,@p2)",JsonSerializer.Serialize(ads),Now,JsonSerializer.Serialize(SupportCatalog.Defaults));
#if !COMMUNITY_APP
            var support=JsonSerializer.Deserialize<List<SupportEntry>>(Text(Rows(c,"SELECT support FROM AppContent WHERE id=1")[0],"support"))??[];bool added=false;
            foreach(var entry in new[]{new SupportEntry("purchase-contact","扫码联系客服购买","扫描客服二维码咨询并购买卡密。","","","",true,10),new SupportEntry("invoice-contact","需要发票","申请发票请添加客服,提供订单和开票资料。","","","",true,11)})
                if(support.All(e=>e.Id!=entry.Id)){support.Add(entry);added=true;}
            if(added)Exec(c,"UPDATE AppContent SET support=@p0,revision=revision+1,updatedAt=@p1",JsonSerializer.Serialize(support),Now);
#endif
            return 0;
        });
    }
    private static void Actor(SqliteConnection c,Account actor,bool admin)
    {
        var row=Rows(c,"SELECT role FROM Accounts WHERE id=@p0 AND active=1",actor.Id).FirstOrDefault()??throw new CommunityException(401,"账户已停用,请重新登录");
        if(admin && Text(row,"role")!="admin")throw new CommunityException(403,"只有服务器管理员可以管理全局内容及客服入口");
    }
    private static AppContent Read(SqliteConnection c,bool admin)
    {
        var row=Rows(c,"SELECT * FROM AppContent WHERE id=1").Single();
        var ads=JsonSerializer.Deserialize<List<Advertisement>>(Text(row,"ads"))??[];
        var support=JsonSerializer.Deserialize<List<SupportEntry>>(Text(row,"support"))??[];
        if(!admin)ads=ads.Where(a=>a.Enabled).ToList();
        if(!admin)support=support.Where(a=>a.Enabled).ToList();
        var about=string.IsNullOrWhiteSpace(Text(row,"about"))?new AboutInfo(ServerProduct.Id=="community"?"寄当家社群为同行提供图文交流、供需发布、服务展示与私信沟通。":"寄当家为门店提供客户档案、业务、库存和财务管理,帮助经营者清楚掌握每一笔往来。","待填写","待填写",""):JsonSerializer.Deserialize<AboutInfo>(Text(row,"about"));
        return new(Convert.ToInt64(row["revision"]),ads.OrderBy(a=>a.SortOrder).ThenBy(a=>a.Id).ToList(),Text(row,"communityUrl"),Text(row,"updatedAt"),support.OrderBy(a=>a.SortOrder).ThenBy(a=>a.Id).ToList(),about);
    }
    public AppContent Public()=>store.Read(c=>Read(c,false) with{Ads=[],CommunityUrl=""});
    public AppContent Get(Account actor,bool admin=false)=>store.Read(c=>{Actor(c,actor,admin);return Read(c,admin);});
    private static string Url(string? value)
    {
        value=(value??"").Trim();if(value.Length==0)return "";
        if(value.Length>2048 || !Uri.TryCreate(value,UriKind.Absolute,out var url) || url.Scheme!="https" || url.UserInfo.Length>0)throw new ArgumentException("跳转地址和社群地址须为 HTTPS 链接");
        return value;
    }
    private static string Image(string? value)
    {
        value??="";if(value.Length==0)return "";
        var comma=value.IndexOf(',');var header=comma>=0?value[..comma]:"";
        if(header is not ("data:image/jpeg;base64" or "data:image/png;base64") || value.Length>1_400_000)throw new ArgumentException("图片应为 JPEG 或 PNG,且不能超过 1 MB");
        byte[] bytes;try{bytes=Convert.FromBase64String(value[(comma+1)..]);}catch(FormatException){throw new ArgumentException("图片数据无效");}
        bool valid=header.Contains("jpeg")?bytes.Length>=3&&bytes[0]==0xff&&bytes[1]==0xd8&&bytes[2]==0xff:bytes.Length>=8&&bytes.AsSpan(0,8).SequenceEqual(new byte[]{137,80,78,71,13,10,26,10});
        if(!valid || bytes.Length>1_048_576)throw new ArgumentException("图片内容或大小无效");return value;
    }
    public AppContent Update(Account actor,UpdateContentRequest request)=>store.Write(c=>
    {
        Actor(c,actor,true);var current=Read(c,true);
        if(current.Revision!=request.ExpectedRevision)throw new CommunityException(409,"后台内容已被更新,请刷新后再修改");
        if(request.Ads is null || request.Ads.Count>20)throw new ArgumentException("最多可配置 20 个广告位");
        var ids=new HashSet<string>();var ads=new List<Advertisement>();
        foreach(var ad in request.Ads)
        {
            if(ad is null || string.IsNullOrWhiteSpace(ad.Id) || ad.Id.Length>64 || !ad.Id.All(ch=>char.IsAsciiLetterOrDigit(ch)||ch is '-' or '_') || !ids.Add(ad.Id))throw new ArgumentException("广告编号无效或重复");
            var title=(ad.Title??"").Trim();var subtitle=(ad.Subtitle??"").Trim();
            if(title.Length is <1 or >100 || subtitle.Length>200 || ad.SortOrder is <0 or >9999)throw new ArgumentException("广告标题为 1 至 100 字,副标题最多 200 字,排序为 0 至 9999");
            ads.Add(ad with{Title=title,Subtitle=subtitle,Image=Image(ad.Image),Link=Url(ad.Link)});
        }
        if(ads.Sum(a=>(long)a.Image.Length)>5_500_000)throw new ArgumentException("广告图片总大小超限,请压缩图片或减少广告数量");
        Exec(c,"UPDATE AppContent SET revision=@p0,ads=@p1,communityUrl=@p2,updatedAt=@p3 WHERE id=1",current.Revision+1,JsonSerializer.Serialize(ads),Url(request.CommunityUrl),Now);
        Exec(c,"INSERT INTO Audit(id,actor,action,target,detail,createdAt) VALUES(@p0,@p1,'CONTENT_UPDATE','AppContent',@p2,@p3)",Guid.NewGuid().ToString("N"),actor.Id,$"更新全局广告 {ads.Count} 条及社群链接",Now);
        return Read(c,true);
    });
    public AppContent UpdateSupport(Account actor,UpdateSupportRequest request)=>store.Write(c=>
    {
        Actor(c,actor,true);var current=Read(c,true);
        if(current.Revision!=request.ExpectedRevision)throw new CommunityException(409,"后台内容已被更新,请刷新后再修改");
        if(request.Entries is null || request.Entries.Count>12)throw new ArgumentException("最多可配置 12 个客服与反馈入口");
        var ids=new HashSet<string>();var entries=new List<SupportEntry>();
        foreach(var entry in request.Entries)
        {
            if(entry is null || string.IsNullOrWhiteSpace(entry.Id) || entry.Id.Length>64 || !entry.Id.All(ch=>char.IsAsciiLetterOrDigit(ch)||ch is '-' or '_') || !ids.Add(entry.Id))throw new ArgumentException("客服入口编号无效或重复");
            var title=(entry.Title??"").Trim();var description=(entry.Description??"").Trim();var phone=(entry.Phone??"").Trim();
            if(title.Length is <1 or >60 || description.Length>1000 || entry.SortOrder is <0 or >9999)throw new ArgumentException("入口标题为 1 至 60 字,说明最多 1000 字,排序为 0 至 9999");
            if(phone.Length>40 || phone.Length>0&&(!phone.Any(char.IsAsciiDigit)||!phone.All(ch=>char.IsAsciiDigit(ch)||ch is '+' or '-' or '(' or ')' or ' ')))throw new ArgumentException("请填写有效的客服电话");
            entries.Add(entry with{Title=title,Description=description,Phone=phone,QrImage=Image(entry.QrImage),Link=Url(entry.Link)});
        }
        if(entries.Sum(e=>(long)e.QrImage.Length)>5_500_000)throw new ArgumentException("二维码图片总大小超限,请压缩图片或减少入口数量");
#if !COMMUNITY_APP
        foreach(var fixedEntry in (current.Support??[]).Where(e=>e.Id is "purchase-contact" or "invoice-contact"))
        {
            if(entries.All(e=>e.Id!=fixedEntry.Id))entries.Add(fixedEntry);
            int index=entries.FindIndex(e=>e.Id==fixedEntry.Id);entries[index]=entries[index] with{Enabled=true};
        }
#endif
        if(entries.Count>12||entries.Sum(e=>(long)e.QrImage.Length)>5_500_000)throw new ArgumentException("客服入口最多 12 个(含固定入口),二维码总大小不能超过 5.5 MB");
        Exec(c,"UPDATE AppContent SET revision=@p0,support=@p1,updatedAt=@p2 WHERE id=1",current.Revision+1,JsonSerializer.Serialize(entries),Now);
        Exec(c,"INSERT INTO Audit(id,actor,action,target,detail,createdAt) VALUES(@p0,@p1,'SUPPORT_UPDATE','AppContent',@p2,@p3)",Guid.NewGuid().ToString("N"),actor.Id,$"更新客服与反馈入口 {entries.Count} 条",Now);
        return Read(c,true);
    });
    public AppContent UpdateAbout(Account actor,UpdateAboutRequest request)=>store.Write(c=>
    {
        Actor(c,actor,true);var current=Read(c,true);if(current.Revision!=request.ExpectedRevision)throw new CommunityException(409,"内容已更新,请刷新后重试");
        var value=request.About??throw new ArgumentException("请填写关于我们");
        string Clean(string? text,int max){text=(text??"").Trim();if(text.Length>max||text.Any(ch=>char.IsControl(ch)&&ch is not '\n' and not '\r' and not '\t'))throw new ArgumentException("内容过长或含无效字符");return text;}
        var about=new AboutInfo(Clean(value.Introduction,5000),Clean(value.Operator,200),Clean(value.Copyright,300),Clean(value.IcpNumber,100));
        Exec(c,"UPDATE AppContent SET about=@p0,revision=revision+1,updatedAt=@p1 WHERE id=1",JsonSerializer.Serialize(about),Now);
        Exec(c,"INSERT INTO Audit(id,actor,action,target,detail,createdAt) VALUES(@p0,@p1,'ABOUT_UPDATE','AppContent','更新关于我们与备案号',@p2)",Guid.NewGuid().ToString("N"),actor.Id,Now);return Read(c,true);
    });
}