using JiDangJia.Contracts; using Microsoft.Data.Sqlite; namespace JiDangJia.Server; public sealed record ClearMyDataRequest(string Password,string PasswordAgain,string ClientId); public sealed record PasswordVerificationRequest(string Password); public sealed partial class ServerStore { private static void SafetyColumn(SqliteConnection c,string table,string column,string definition) {if(!Rows(c,"PRAGMA table_info("+table+")").Any(r=>Text(r,"name")==column))Exec(c,$"ALTER TABLE {table} ADD COLUMN {column} {definition}");} private static void InitializeSafety(SqliteConnection c) { foreach(var (column,definition) in new[]{("phone","TEXT NOT NULL DEFAULT ''"),("phoneMethod","TEXT NOT NULL DEFAULT ''"),("identityStatus","TEXT NOT NULL DEFAULT 'NONE'"),("identityKind","TEXT NOT NULL DEFAULT 'PERSONAL'"),("communityBanned","INTEGER NOT NULL DEFAULT 0"),("communityBanReason","TEXT NOT NULL DEFAULT ''")})SafetyColumn(c,"Accounts",column,definition); #if !COMMUNITY_APP SafetyColumn(c,"Cards","permanent","INTEGER NOT NULL DEFAULT 0");SafetyColumn(c,"Cards","fixedUntil","TEXT");SafetyColumn(c,"Licenses","permanent","INTEGER NOT NULL DEFAULT 0"); #endif SafetyColumn(c,"Accounts","communityBanUntil","TEXT NOT NULL DEFAULT ''"); Exec(c,""" CREATE TABLE IF NOT EXISTS CommunityPolicy(id INTEGER PRIMARY KEY CHECK(id=1),version INTEGER NOT NULL,text TEXT NOT NULL,publishedAt TEXT NOT NULL,purchaseUrl TEXT NOT NULL DEFAULT ''); CREATE TABLE IF NOT EXISTS CommunityAcceptances(accountId TEXT PRIMARY KEY,version INTEGER NOT NULL,acceptedAt TEXT NOT NULL); CREATE TABLE IF NOT EXISTS AccountChatClears(accountId TEXT PRIMARY KEY,throughId INTEGER NOT NULL,clearedAt TEXT NOT NULL); CREATE TABLE IF NOT EXISTS AccountDataClears(accountId TEXT NOT NULL,clientId TEXT NOT NULL,throughId INTEGER NOT NULL,createdAt TEXT NOT NULL,PRIMARY KEY(accountId,clientId)); """); Exec(c,"INSERT OR IGNORE INTO CommunityPolicy(id,version,text,publishedAt) VALUES(1,1,@p0,@p1)",CommunitySafetyDefaults.Agreement,Now); } internal static Dictionary SafetyAccount(SqliteConnection c,string id) =>Rows(c,"SELECT * FROM Accounts WHERE id=@p0 AND active=1",id).FirstOrDefault()??throw new CommunityException(401,"账户不可用,请重新登录"); internal static void SocialRead(SqliteConnection c,Account account) { SafetyAccount(c,account.Id); if(Rows(c,"SELECT a.accountId FROM CommunityAcceptances a JOIN CommunityPolicy p ON a.version=p.version WHERE a.accountId=@p0",account.Id).Count==0)throw new CommunityException(428,"请先阅读并勾选同意最新社群用户协议"); } internal static void SocialWrite(SqliteConnection c,Account account,string? peer=null) { SocialRead(c,account);var row=SafetyAccount(c,account.Id); if(CommunityMuted(row))throw new CommunityException(403,"社群已禁言"+(Text(row,"communityBanUntil").Length==0?"(永久)":",截止 "+Text(row,"communityBanUntil"))+":"+Text(row,"communityBanReason")); if(Text(row,"identityStatus")!="APPROVED"||Text(row,"phone").Length==0)throw new CommunityException(403,"浏览无需认证;发布或聊天请先完成个人 / 企业认证及手机号绑定"); if(peer is not null){var other=SafetyAccount(c,peer);SocialRead(c,AccountFrom(other));if(Text(other,"identityStatus")!="APPROVED"||Text(other,"phone").Length==0)throw new CommunityException(403,"对方尚未完成认证及手机号绑定");} } internal static bool CommunityMuted(Dictionary row) {var until=Text(row,"communityBanUntil");return Text(row,"communityBanned")=="1"&&(until.Length==0||!DateTimeOffset.TryParse(until,out var end)||end>DateTimeOffset.UtcNow);} public CommunityAgreement Agreement()=>Read(c=>{var r=Rows(c,"SELECT * FROM CommunityPolicy WHERE id=1")[0];return new CommunityAgreement(Convert.ToInt64(r["version"]),Text(r,"text"),Text(r,"publishedAt"));}); public CommunityAccess Access(Account account)=>Read(c=> { var r=SafetyAccount(c,account.Id);var p=Rows(c,"SELECT * FROM CommunityPolicy WHERE id=1")[0];var accepted=Rows(c,"SELECT accountId FROM CommunityAcceptances WHERE accountId=@p0 AND version=@p1",account.Id,p["version"]).Count>0; bool muted=CommunityMuted(r);return new CommunityAccess(muted,muted?Text(r,"communityBanReason"):"",accepted,Convert.ToInt64(p["version"]),Text(r,"identityStatus"),Text(r,"identityKind"),Text(r,"phone").Length>0,muted?Text(r,"communityBanUntil"):""); }); public CommunityAccess AcceptAgreement(Account account,AcceptAgreementRequest request) { Write(c=>{SafetyAccount(c,account.Id);var current=Convert.ToInt64(Rows(c,"SELECT version FROM CommunityPolicy WHERE id=1")[0]["version"]);if(!request.Accepted)throw new ArgumentException("需主动勾选同意协议");if(current!=request.Version)throw new CommunityException(409,"协议版本已更新,请重新阅读");Exec(c,"INSERT INTO CommunityAcceptances VALUES(@p0,@p1,@p2) ON CONFLICT(accountId) DO UPDATE SET version=excluded.version,acceptedAt=excluded.acceptedAt",account.Id,current,Now);Audit(c,account.Id,"SOCIAL_AGREEMENT",current.ToString(),"");return 0;});return Access(account); } public CommunityGovernance Governance(Account actor) {RequireAdmin(actor);return Read(c=>{var r=Rows(c,"SELECT * FROM CommunityPolicy WHERE id=1")[0];return new CommunityGovernance(new(Convert.ToInt64(r["version"]),Text(r,"text"),Text(r,"publishedAt")),"DIRECT");});} public CommunityGovernance UpdateGovernance(Account actor,UpdateGovernanceRequest request) { RequireAdmin(actor);var text=(request.AgreementText??"").Trim();var url="";if(text.Length is <20 or >16000)throw new ArgumentException("协议正文需要 20–16000 字"); Write(c=>{var current=Rows(c,"SELECT * FROM CommunityPolicy WHERE id=1")[0];if(Convert.ToInt64(current["version"])!=request.ExpectedVersion)throw new CommunityException(409,"配置已更新,请刷新后修改");bool changed=Text(current,"text")!=text;Exec(c,"UPDATE CommunityPolicy SET version=version+@p0,text=@p1,publishedAt=CASE WHEN @p0=1 THEN @p2 ELSE publishedAt END,purchaseUrl=@p3 WHERE id=1",changed?1:0,text,Now,url);Audit(c,actor.Id,"SOCIAL_POLICY",request.ExpectedVersion.ToString(),changed?"新协议版本":"协议设置");return 0;});return Governance(actor); } public void SetCommunityBan(Account admin,string id,CommunityBanRequest request) { RequireAdmin(admin);var reason=(request.Reason??"").Trim();if(request.Banned&&reason.Length is <1 or >500)throw new ArgumentException("禁言原因需为 1–500 字"); if(request.Banned&&request.Days is <1 or >36500)throw new ArgumentException("禁言天数需为 1–36500,永久禁言不填写天数"); string until=request.Banned&&request.Days.HasValue?DateTimeOffset.UtcNow.AddDays(request.Days.Value).ToString("O"):""; Write(c=>{var row=SafetyAccount(c,id);if(Text(row,"role")=="admin")throw new ArgumentException("管理员不可通过此处禁言");Exec(c,"UPDATE Accounts SET communityBanned=@p0,communityBanReason=@p1,communityBanUntil=@p2 WHERE id=@p3",request.Banned?1:0,request.Banned?reason:"",until,id);Audit(c,admin.Id,"SOCIAL_BAN",id,request.Banned?reason+";"+(until.Length==0?"永久":"截止 "+until):"解除禁言");return 0;}); } public void VerifyOwnPassword(Account account,string password)=>Read(c=>{CheckPassword(c,account,password);return 0;}); private static void CheckPassword(SqliteConnection c,Account account,string? password) {var row=SafetyAccount(c,account.Id);if(password is null||password.Length is <8 or >128||!VerifyPassword(password,Text(row,"passwordHash")))throw new CommunityException(403,"登录密码不正确");} public object ClearOwnChat(Account account,ClearMyDataRequest request)=>Write(c=> { CheckPassword(c,account,request.Password);CheckPassword(c,account,request.PasswordAgain); if(request.ClientId is null||!System.Text.RegularExpressions.Regex.IsMatch(request.ClientId,"^[a-f0-9]{32}$"))throw new ArgumentException("清除请求标识无效"); var previous=Rows(c,"SELECT throughId FROM AccountDataClears WHERE accountId=@p0 AND clientId=@p1",account.Id,request.ClientId).FirstOrDefault(); long through=previous is null?Convert.ToInt64(Rows(c,"SELECT COALESCE(MAX(id),0) id FROM ChatMessages WHERE senderId=@p0 OR recipientId=@p0",account.Id)[0]["id"]):Convert.ToInt64(previous["throughId"]); Exec(c,"INSERT INTO AccountChatClears VALUES(@p0,@p1,@p2) ON CONFLICT(accountId) DO UPDATE SET throughId=MAX(throughId,excluded.throughId),clearedAt=excluded.clearedAt",account.Id,through,Now); Exec(c,"INSERT OR IGNORE INTO AccountDataClears VALUES(@p0,@p1,@p2,@p3)",account.Id,request.ClientId,through,Now); Exec(c,"DELETE FROM ChatReads WHERE accountId=@p0",account.Id);Exec(c,"DELETE FROM ChatReadMessages WHERE accountId=@p0",account.Id);Exec(c,"DELETE FROM ChatDeliveries WHERE accountId=@p0 AND messageId<=@p1",account.Id,through); Audit(c,account.Id,"OWN_DATA_CLEAR",account.Id,"仅清除此账户可见聊天历史;社群本机缓存由设备清除");return new{ok=true,accountId=account.Id,clientId=request.ClientId,throughId=through}; }); }