2020-05-28 19:08:45 +08:00
|
|
|
// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
|
|
|
|
// See the LICENCE file in the repository root for full licence text.
|
|
|
|
|
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
2020-07-31 20:39:50 +08:00
|
|
|
using System.Linq;
|
|
|
|
using System.Net;
|
|
|
|
using JetBrains.Annotations;
|
|
|
|
using Newtonsoft.Json.Linq;
|
2020-05-28 19:08:45 +08:00
|
|
|
using NUnit.Framework;
|
2020-07-31 20:39:50 +08:00
|
|
|
using osu.Framework.Graphics.Containers;
|
|
|
|
using osu.Framework.Testing;
|
|
|
|
using osu.Game.Graphics.Containers;
|
|
|
|
using osu.Game.Graphics.UserInterface;
|
2020-05-28 19:08:45 +08:00
|
|
|
using osu.Game.Online.API;
|
2020-07-31 20:39:50 +08:00
|
|
|
using osu.Game.Online.API.Requests;
|
2021-11-04 17:02:44 +08:00
|
|
|
using osu.Game.Online.API.Requests.Responses;
|
2020-12-25 12:38:11 +08:00
|
|
|
using osu.Game.Online.Rooms;
|
2020-05-28 19:08:45 +08:00
|
|
|
using osu.Game.Rulesets.Osu;
|
|
|
|
using osu.Game.Rulesets.Scoring;
|
|
|
|
using osu.Game.Scoring;
|
2020-12-25 23:50:00 +08:00
|
|
|
using osu.Game.Screens.OnlinePlay.Playlists;
|
2020-07-31 20:39:50 +08:00
|
|
|
using osu.Game.Screens.Ranking;
|
2020-05-28 19:08:45 +08:00
|
|
|
using osu.Game.Tests.Beatmaps;
|
2021-12-13 15:34:48 +08:00
|
|
|
using osu.Game.Tests.Resources;
|
2020-05-28 19:08:45 +08:00
|
|
|
|
2020-12-25 12:20:37 +08:00
|
|
|
namespace osu.Game.Tests.Visual.Playlists
|
2020-05-28 19:08:45 +08:00
|
|
|
{
|
2020-12-25 12:11:21 +08:00
|
|
|
public class TestScenePlaylistsResultsScreen : ScreenTestScene
|
2020-05-28 19:08:45 +08:00
|
|
|
{
|
2020-07-31 20:39:50 +08:00
|
|
|
private const int scores_per_result = 10;
|
2021-12-13 05:54:57 +08:00
|
|
|
private const int real_user_position = 200;
|
2020-07-31 20:39:50 +08:00
|
|
|
|
|
|
|
private TestResultsScreen resultsScreen;
|
2021-12-10 14:28:41 +08:00
|
|
|
|
2020-07-31 20:39:50 +08:00
|
|
|
private int currentScoreId;
|
|
|
|
private bool requestComplete;
|
2021-10-08 14:18:01 +08:00
|
|
|
private int totalCount;
|
2020-06-09 17:53:55 +08:00
|
|
|
|
|
|
|
[SetUp]
|
|
|
|
public void Setup() => Schedule(() =>
|
|
|
|
{
|
2021-12-10 14:28:41 +08:00
|
|
|
currentScoreId = 1;
|
2020-07-31 20:39:50 +08:00
|
|
|
requestComplete = false;
|
2021-10-08 14:18:01 +08:00
|
|
|
totalCount = 0;
|
2020-06-09 17:53:55 +08:00
|
|
|
bindHandler();
|
2022-01-18 15:48:27 +08:00
|
|
|
|
|
|
|
// beatmap is required to be an actual beatmap so the scores can get their scores correctly calculated for standardised scoring.
|
|
|
|
// else the tests that rely on ordering will fall over.
|
|
|
|
Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value);
|
2020-06-09 17:53:55 +08:00
|
|
|
});
|
|
|
|
|
2020-05-28 19:08:45 +08:00
|
|
|
[Test]
|
2020-07-31 20:39:50 +08:00
|
|
|
public void TestShowWithUserScore()
|
2020-05-28 21:25:00 +08:00
|
|
|
{
|
2020-07-31 20:39:50 +08:00
|
|
|
ScoreInfo userScore = null;
|
|
|
|
|
|
|
|
AddStep("bind user score info handler", () =>
|
|
|
|
{
|
2021-12-13 15:37:20 +08:00
|
|
|
userScore = TestResources.CreateTestScoreInfo();
|
2021-12-13 15:34:48 +08:00
|
|
|
userScore.OnlineID = currentScoreId++;
|
|
|
|
|
2020-07-31 20:39:50 +08:00
|
|
|
bindHandler(userScore: userScore);
|
|
|
|
});
|
|
|
|
|
|
|
|
createResults(() => userScore);
|
|
|
|
|
2021-12-10 14:37:12 +08:00
|
|
|
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded);
|
2021-12-13 05:54:57 +08:00
|
|
|
AddAssert($"score panel position is {real_user_position}",
|
|
|
|
() => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).ScorePosition.Value == real_user_position);
|
2020-05-28 21:25:00 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
[Test]
|
2020-07-31 20:39:50 +08:00
|
|
|
public void TestShowNullUserScore()
|
2020-05-28 21:25:00 +08:00
|
|
|
{
|
2020-07-31 20:39:50 +08:00
|
|
|
createResults();
|
|
|
|
|
|
|
|
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
|
2020-06-09 17:53:55 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
[Test]
|
2020-07-31 20:39:50 +08:00
|
|
|
public void TestShowUserScoreWithDelay()
|
|
|
|
{
|
|
|
|
ScoreInfo userScore = null;
|
|
|
|
|
|
|
|
AddStep("bind user score info handler", () =>
|
|
|
|
{
|
2021-12-13 15:37:20 +08:00
|
|
|
userScore = TestResources.CreateTestScoreInfo();
|
2021-12-13 15:34:48 +08:00
|
|
|
userScore.OnlineID = currentScoreId++;
|
|
|
|
|
2021-02-22 14:43:58 +08:00
|
|
|
bindHandler(true, userScore);
|
2020-07-31 20:39:50 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
createResults(() => userScore);
|
|
|
|
|
|
|
|
AddAssert("more than 1 panel displayed", () => this.ChildrenOfType<ScorePanel>().Count() > 1);
|
2021-12-10 14:37:12 +08:00
|
|
|
AddAssert("user score selected", () => this.ChildrenOfType<ScorePanel>().Single(p => p.Score.OnlineID == userScore.OnlineID).State == PanelState.Expanded);
|
2020-07-31 20:39:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
public void TestShowNullUserScoreWithDelay()
|
2020-06-09 17:53:55 +08:00
|
|
|
{
|
2021-02-22 14:43:58 +08:00
|
|
|
AddStep("bind delayed handler", () => bindHandler(true));
|
2020-07-31 20:39:50 +08:00
|
|
|
|
|
|
|
createResults();
|
|
|
|
|
|
|
|
AddAssert("top score selected", () => this.ChildrenOfType<ScorePanel>().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded);
|
2020-05-28 21:25:00 +08:00
|
|
|
}
|
|
|
|
|
2020-07-31 20:39:50 +08:00
|
|
|
[Test]
|
|
|
|
public void TestFetchWhenScrolledToTheRight()
|
|
|
|
{
|
|
|
|
createResults();
|
|
|
|
|
2021-02-22 14:43:58 +08:00
|
|
|
AddStep("bind delayed handler", () => bindHandler(true));
|
2020-07-31 20:39:50 +08:00
|
|
|
|
|
|
|
for (int i = 0; i < 2; i++)
|
|
|
|
{
|
|
|
|
int beforePanelCount = 0;
|
|
|
|
|
|
|
|
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
|
|
|
|
AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToEnd(false));
|
|
|
|
|
|
|
|
AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible);
|
|
|
|
waitForDisplay();
|
|
|
|
|
|
|
|
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
|
|
|
|
AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
[Test]
|
|
|
|
public void TestFetchWhenScrolledToTheLeft()
|
|
|
|
{
|
|
|
|
ScoreInfo userScore = null;
|
|
|
|
|
|
|
|
AddStep("bind user score info handler", () =>
|
|
|
|
{
|
2021-12-13 15:37:20 +08:00
|
|
|
userScore = TestResources.CreateTestScoreInfo();
|
2021-12-13 15:34:48 +08:00
|
|
|
userScore.OnlineID = currentScoreId++;
|
|
|
|
|
2020-07-31 20:39:50 +08:00
|
|
|
bindHandler(userScore: userScore);
|
|
|
|
});
|
|
|
|
|
|
|
|
createResults(() => userScore);
|
|
|
|
|
2021-02-22 14:43:58 +08:00
|
|
|
AddStep("bind delayed handler", () => bindHandler(true));
|
2020-07-31 20:39:50 +08:00
|
|
|
|
|
|
|
for (int i = 0; i < 2; i++)
|
|
|
|
{
|
|
|
|
int beforePanelCount = 0;
|
|
|
|
|
|
|
|
AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType<ScorePanel>().Count());
|
|
|
|
AddStep("scroll to left", () => resultsScreen.ScorePanelList.ChildrenOfType<OsuScrollContainer>().Single().ScrollToStart(false));
|
|
|
|
|
|
|
|
AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible);
|
|
|
|
waitForDisplay();
|
|
|
|
|
|
|
|
AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType<ScorePanel>().Count() == beforePanelCount + scores_per_result);
|
|
|
|
AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void createResults(Func<ScoreInfo> getScore = null)
|
2020-06-09 17:53:55 +08:00
|
|
|
{
|
|
|
|
AddStep("load results", () =>
|
|
|
|
{
|
2020-07-31 20:39:50 +08:00
|
|
|
LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem
|
2020-06-09 17:53:55 +08:00
|
|
|
{
|
|
|
|
Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo },
|
|
|
|
Ruleset = { Value = new OsuRuleset().RulesetInfo }
|
|
|
|
}));
|
|
|
|
});
|
2021-09-06 19:20:52 +08:00
|
|
|
|
2021-12-20 15:58:16 +08:00
|
|
|
AddUntilStep("wait for screen to load", () => resultsScreen.IsLoaded);
|
2021-10-08 14:18:01 +08:00
|
|
|
waitForDisplay();
|
2020-06-09 17:53:55 +08:00
|
|
|
}
|
|
|
|
|
2020-07-31 20:39:50 +08:00
|
|
|
private void waitForDisplay()
|
2020-05-28 19:08:45 +08:00
|
|
|
{
|
2021-12-20 15:58:16 +08:00
|
|
|
AddUntilStep("wait for scores loaded", () =>
|
2021-10-08 14:18:01 +08:00
|
|
|
requestComplete
|
|
|
|
&& resultsScreen.ScorePanelList.GetScorePanels().Count() == totalCount
|
|
|
|
&& resultsScreen.ScorePanelList.AllPanelsVisible);
|
2020-07-31 20:39:50 +08:00
|
|
|
AddWaitStep("wait for display", 5);
|
|
|
|
}
|
|
|
|
|
2021-02-22 14:43:58 +08:00
|
|
|
private void bindHandler(bool delayed = false, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request =>
|
2020-07-31 20:39:50 +08:00
|
|
|
{
|
2021-03-23 17:08:32 +08:00
|
|
|
// pre-check for requests we should be handling (as they are scheduled below).
|
|
|
|
switch (request)
|
|
|
|
{
|
|
|
|
case ShowPlaylistUserScoreRequest _:
|
|
|
|
case IndexPlaylistScoresRequest _:
|
|
|
|
break;
|
|
|
|
|
|
|
|
default:
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2020-07-31 20:39:50 +08:00
|
|
|
requestComplete = false;
|
|
|
|
|
2021-02-22 14:43:58 +08:00
|
|
|
double delay = delayed ? 3000 : 0;
|
2020-07-31 20:39:50 +08:00
|
|
|
|
2021-02-22 14:43:58 +08:00
|
|
|
Scheduler.AddDelayed(() =>
|
2020-07-31 20:39:50 +08:00
|
|
|
{
|
2021-02-22 14:43:58 +08:00
|
|
|
if (failRequests)
|
|
|
|
{
|
|
|
|
triggerFail(request);
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (request)
|
|
|
|
{
|
|
|
|
case ShowPlaylistUserScoreRequest s:
|
|
|
|
if (userScore == null)
|
|
|
|
triggerFail(s);
|
|
|
|
else
|
|
|
|
triggerSuccess(s, createUserResponse(userScore));
|
2021-10-08 14:18:01 +08:00
|
|
|
|
2021-02-22 14:43:58 +08:00
|
|
|
break;
|
|
|
|
|
|
|
|
case IndexPlaylistScoresRequest i:
|
|
|
|
triggerSuccess(i, createIndexResponse(i));
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}, delay);
|
2021-03-23 17:08:32 +08:00
|
|
|
|
|
|
|
return true;
|
2020-07-31 20:39:50 +08:00
|
|
|
};
|
2020-05-28 19:08:45 +08:00
|
|
|
|
2021-02-22 14:43:58 +08:00
|
|
|
private void triggerSuccess<T>(APIRequest<T> req, T result)
|
2020-07-31 20:39:50 +08:00
|
|
|
where T : class
|
|
|
|
{
|
2021-02-22 14:43:58 +08:00
|
|
|
requestComplete = true;
|
|
|
|
req.TriggerSuccess(result);
|
2020-07-31 20:39:50 +08:00
|
|
|
}
|
|
|
|
|
2021-02-22 14:43:58 +08:00
|
|
|
private void triggerFail(APIRequest req)
|
2020-07-31 20:39:50 +08:00
|
|
|
{
|
2021-02-22 14:43:58 +08:00
|
|
|
requestComplete = true;
|
|
|
|
req.TriggerFailure(new WebException("Failed."));
|
2020-07-31 20:39:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
private MultiplayerScore createUserResponse([NotNull] ScoreInfo userScore)
|
|
|
|
{
|
|
|
|
var multiplayerUserScore = new MultiplayerScore
|
|
|
|
{
|
2021-12-10 14:28:41 +08:00
|
|
|
ID = (int)(userScore.OnlineID > 0 ? userScore.OnlineID : currentScoreId++),
|
2020-07-31 20:39:50 +08:00
|
|
|
Accuracy = userScore.Accuracy,
|
|
|
|
EndedAt = userScore.Date,
|
|
|
|
Passed = userScore.Passed,
|
|
|
|
Rank = userScore.Rank,
|
2021-12-13 05:54:57 +08:00
|
|
|
Position = real_user_position,
|
2020-07-31 20:39:50 +08:00
|
|
|
MaxCombo = userScore.MaxCombo,
|
|
|
|
TotalScore = userScore.TotalScore,
|
|
|
|
User = userScore.User,
|
|
|
|
Statistics = userScore.Statistics,
|
|
|
|
ScoresAround = new MultiplayerScoresAround
|
|
|
|
{
|
|
|
|
Higher = new MultiplayerScores(),
|
|
|
|
Lower = new MultiplayerScores()
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2021-10-08 14:18:01 +08:00
|
|
|
totalCount++;
|
|
|
|
|
2020-07-31 20:39:50 +08:00
|
|
|
for (int i = 1; i <= scores_per_result; i++)
|
|
|
|
{
|
|
|
|
multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore
|
|
|
|
{
|
|
|
|
ID = currentScoreId++,
|
|
|
|
Accuracy = userScore.Accuracy,
|
|
|
|
EndedAt = userScore.Date,
|
|
|
|
Passed = true,
|
|
|
|
Rank = userScore.Rank,
|
|
|
|
MaxCombo = userScore.MaxCombo,
|
|
|
|
TotalScore = userScore.TotalScore - i,
|
2021-11-04 17:02:44 +08:00
|
|
|
User = new APIUser
|
2020-07-31 20:39:50 +08:00
|
|
|
{
|
|
|
|
Id = 2,
|
|
|
|
Username = $"peppy{i}",
|
|
|
|
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
|
|
|
},
|
|
|
|
Statistics = userScore.Statistics
|
|
|
|
});
|
|
|
|
|
|
|
|
multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore
|
|
|
|
{
|
|
|
|
ID = currentScoreId++,
|
|
|
|
Accuracy = userScore.Accuracy,
|
|
|
|
EndedAt = userScore.Date,
|
|
|
|
Passed = true,
|
|
|
|
Rank = userScore.Rank,
|
|
|
|
MaxCombo = userScore.MaxCombo,
|
|
|
|
TotalScore = userScore.TotalScore + i,
|
2021-11-04 17:02:44 +08:00
|
|
|
User = new APIUser
|
2020-07-31 20:39:50 +08:00
|
|
|
{
|
|
|
|
Id = 2,
|
|
|
|
Username = $"peppy{i}",
|
|
|
|
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
|
|
|
},
|
|
|
|
Statistics = userScore.Statistics
|
|
|
|
});
|
2021-10-08 14:18:01 +08:00
|
|
|
|
|
|
|
totalCount += 2;
|
2020-07-31 20:39:50 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
addCursor(multiplayerUserScore.ScoresAround.Lower);
|
|
|
|
addCursor(multiplayerUserScore.ScoresAround.Higher);
|
|
|
|
|
|
|
|
return multiplayerUserScore;
|
|
|
|
}
|
|
|
|
|
|
|
|
private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req)
|
|
|
|
{
|
|
|
|
var result = new IndexedMultiplayerScores();
|
|
|
|
|
|
|
|
long startTotalScore = req.Cursor?.Properties["total_score"].ToObject<long>() ?? 1000000;
|
|
|
|
string sort = req.IndexParams?.Properties["sort"].ToObject<string>() ?? "score_desc";
|
|
|
|
|
|
|
|
for (int i = 1; i <= scores_per_result; i++)
|
|
|
|
{
|
|
|
|
result.Scores.Add(new MultiplayerScore
|
|
|
|
{
|
|
|
|
ID = currentScoreId++,
|
|
|
|
Accuracy = 1,
|
|
|
|
EndedAt = DateTimeOffset.Now,
|
2020-05-28 19:08:45 +08:00
|
|
|
Passed = true,
|
2020-07-31 20:39:50 +08:00
|
|
|
Rank = ScoreRank.X,
|
|
|
|
MaxCombo = 1000,
|
|
|
|
TotalScore = startTotalScore + (sort == "score_asc" ? i : -i),
|
2021-11-04 17:02:44 +08:00
|
|
|
User = new APIUser
|
2020-05-28 19:08:45 +08:00
|
|
|
{
|
|
|
|
Id = 2,
|
|
|
|
Username = $"peppy{i}",
|
|
|
|
CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg",
|
|
|
|
},
|
2020-07-31 20:39:50 +08:00
|
|
|
Statistics = new Dictionary<HitResult, int>
|
2020-05-28 19:08:45 +08:00
|
|
|
{
|
|
|
|
{ HitResult.Miss, 1 },
|
|
|
|
{ HitResult.Meh, 50 },
|
|
|
|
{ HitResult.Good, 100 },
|
2020-07-31 20:39:50 +08:00
|
|
|
{ HitResult.Great, 300 }
|
2020-05-28 19:08:45 +08:00
|
|
|
}
|
|
|
|
});
|
2021-10-08 14:18:01 +08:00
|
|
|
|
|
|
|
totalCount++;
|
2020-05-28 19:08:45 +08:00
|
|
|
}
|
|
|
|
|
2020-07-31 20:39:50 +08:00
|
|
|
addCursor(result);
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void addCursor(MultiplayerScores scores)
|
|
|
|
{
|
|
|
|
scores.Cursor = new Cursor
|
|
|
|
{
|
|
|
|
Properties = new Dictionary<string, JToken>
|
|
|
|
{
|
|
|
|
{ "total_score", JToken.FromObject(scores.Scores[^1].TotalScore) },
|
|
|
|
{ "score_id", JToken.FromObject(scores.Scores[^1].ID) },
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
scores.Params = new IndexScoresParams
|
2020-05-28 19:08:45 +08:00
|
|
|
{
|
2020-07-31 20:39:50 +08:00
|
|
|
Properties = new Dictionary<string, JToken>
|
2020-05-28 19:08:45 +08:00
|
|
|
{
|
2020-07-31 20:39:50 +08:00
|
|
|
{ "sort", JToken.FromObject(scores.Scores[^1].TotalScore > scores.Scores[^2].TotalScore ? "score_asc" : "score_desc") }
|
2020-06-09 17:53:55 +08:00
|
|
|
}
|
|
|
|
};
|
2020-05-28 19:08:45 +08:00
|
|
|
}
|
2020-07-31 20:39:50 +08:00
|
|
|
|
2020-12-25 12:11:21 +08:00
|
|
|
private class TestResultsScreen : PlaylistsResultsScreen
|
2020-07-31 20:39:50 +08:00
|
|
|
{
|
|
|
|
public new LoadingSpinner LeftSpinner => base.LeftSpinner;
|
|
|
|
public new LoadingSpinner CentreSpinner => base.CentreSpinner;
|
|
|
|
public new LoadingSpinner RightSpinner => base.RightSpinner;
|
|
|
|
public new ScorePanelList ScorePanelList => base.ScorePanelList;
|
|
|
|
|
|
|
|
public TestResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true)
|
|
|
|
: base(score, roomId, playlistItem, allowRetry)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
}
|
2020-05-28 19:08:45 +08:00
|
|
|
}
|
|
|
|
}
|