simplify sign calculation

This commit is contained in:
Pig Fang 2020-06-01 16:59:42 +08:00
parent ecc3d02167
commit cf95b3a345
7 changed files with 115 additions and 182 deletions

View File

@ -23,6 +23,7 @@ class UserController extends Controller
{
/** @var User */
$user = auth()->user();
return $user
->makeHidden(['password', 'ip', 'remember_token', 'verification_token']);
}
@ -31,11 +32,11 @@ class UserController extends Controller
{
$user = Auth::user();
[$from, $to] = explode(',', option('sign_score'));
[$min, $max] = explode(',', option('sign_score'));
$scoreIntro = trans('user.score-intro.introduction', [
'initial_score' => option('user_initial_score'),
'score-from' => $from,
'score-to' => $to,
'score-from' => $min,
'score-to' => $max,
'return-score' => option('return_score')
? trans('user.score-intro.will-return-score')
: trans('user.score-intro.no-return-score'),
@ -60,10 +61,6 @@ class UserController extends Controller
$parsedown = new Parsedown();
return view('user.index')->with([
'statistics' => [
'players' => $this->calculatePercentageUsed($user->players->count(), option('score_per_player')),
'storage' => $this->calculatePercentageUsed($this->getStorageUsed($user), option('score_per_storage')),
],
'score_intro' => $scoreIntro,
'rates' => [
'storage' => option('score_per_storage'),
@ -80,89 +77,55 @@ class UserController extends Controller
{
$user = Auth::user();
return json('', 0, [
return response()->json([
'user' => [
'score' => $user->score,
'lastSignAt' => $user->last_sign_at,
],
'stats' => [
'players' => $this->calculatePercentageUsed($user->players->count(), option('score_per_player')),
'storage' => $this->calculatePercentageUsed($this->getStorageUsed($user), option('score_per_storage')),
'rate' => [
'storage' => (int) option('score_per_storage'),
'players' => (int) option('score_per_player'),
],
'usage' => [
'players' => $user->players->count(),
'storage' => (int) Texture::where('uploader', $user->uid)->sum('size'),
],
'signAfterZero' => (bool) option('sign_after_zero'),
'signGapTime' => (int) option('sign_gap_time'),
]);
}
protected function calculatePercentageUsed(int $used, int $rate): array
{
$user = Auth::user();
// Initialize default value to avoid division by zero.
$result['used'] = $used;
$result['total'] = 'UNLIMITED';
$result['percentage'] = 0;
if ($rate != 0) {
$result['total'] = $used + floor($user->score / $rate);
$result['percentage'] = $result['total'] ? $used / $result['total'] * 100 : 100;
}
return $result;
}
protected function getStorageUsed(User $user)
{
return Texture::where('uploader', $user->uid)->select('size')->sum('size');
}
public function sign()
{
/** @var User */
$user = Auth::user();
if ($this->getSignRemainingTime($user) <= 0) {
$acquiredScore = rand(...explode(',', option('sign_score')));
$lastSignTime = Carbon::parse($user->last_sign_at);
$remainingTime = option('sign_after_zero')
? Carbon::now()->diffInSeconds(
$lastSignTime <= Carbon::today() ? $lastSignTime : Carbon::tomorrow(),
false
)
: Carbon::now()->diffInSeconds(
$lastSignTime->addHours((int) option('sign_gap_time')),
false
);
if ($remainingTime <= 0) {
[$min, $max] = explode(',', option('sign_score'));
$acquiredScore = rand((int) $min, (int) $max);
$user->score += $acquiredScore;
$user->last_sign_at = Carbon::now();
$user->save();
$gap = option('sign_gap_time');
return json(trans('user.sign-success', ['score' => $acquiredScore]), 0, [
'score' => $user->score,
'storage' => $this->calculatePercentageUsed($this->getStorageUsed($user), option('score_per_storage')),
'remaining_time' => $gap > 1 ? round($gap) : $gap,
]);
} else {
$remaining_time = $this->getUserSignRemainingTimeWithPrecision($user);
return json(trans('user.cant-sign-until', [
'time' => $remaining_time >= 1
? $remaining_time : round($remaining_time * 60),
'unit' => $remaining_time >= 1
? trans('user.time-unit-hour') : trans('user.time-unit-min'),
]), 1);
return json('', 1);
}
}
protected function getUserSignRemainingTimeWithPrecision(User $user)
{
$hours = $this->getSignRemainingTime($user) / 3600;
return $hours > 1 ? round($hours) : $hours;
}
protected function getSignRemainingTime(User $user)
{
$lastSignTime = Carbon::parse($user->last_sign_at);
if (option('sign_after_zero')) {
return Carbon::now()->diffInSeconds(
$lastSignTime <= Carbon::today() ? $lastSignTime : Carbon::tomorrow(),
false
);
}
return Carbon::now()->diffInSeconds($lastSignTime->addHours(option('sign_gap_time')), false);
}
public function sendVerificationEmail()
{
if (!option('require_verification')) {
@ -187,7 +150,6 @@ class UserController extends Controller
try {
Mail::to($user->email)->send(new EmailVerification(url($url)));
} catch (\Exception $e) {
// Write the exception to log
report($e);
return json(trans('user.verification.failed', ['msg' => $e->getMessage()]), 2);
@ -231,6 +193,7 @@ class UserController extends Controller
public function handleProfile(Request $request, Filter $filter, Dispatcher $dispatcher)
{
$action = $request->input('action', '');
/** @var User */
$user = Auth::user();
$addition = $request->except('action');
@ -333,6 +296,7 @@ class UserController extends Controller
{
$request->validate(['tid' => 'required|integer']);
$tid = $request->input('tid');
/** @var User */
$user = auth()->user();
$can = $filter->apply('user_can_update_avatar', true, [$user, $tid]);

View File

@ -5,12 +5,13 @@ interface Props {
icon: string
color: string
used: number
total: number
unused: number
unit: string
}
const InfoBox: React.FC<Props> = (props) => {
const percentage = (props.used / props.total) * 100
const total = ~~(props.used + props.unused)
const percentage = (props.used / total) * 100
return (
<div className={`info-box bg-${props.color}`}>
@ -20,7 +21,7 @@ const InfoBox: React.FC<Props> = (props) => {
<div className="info-box-content">
<span className="info-box-text">{props.name}</span>
<span className="info-box-number">
<b>{props.used}</b> / {props.total} {props.unit}
<b>{props.used}</b> / {total} {props.unit}
</span>
<div className="progress">
<div className="progress-bar" style={{ width: `${percentage}%` }} />

View File

@ -9,22 +9,18 @@ import urls from '@/scripts/urls'
import * as breakpoints from '@/styles/breakpoints'
import InfoBox from './InfoBox'
import SignButton from './SignButton'
import * as scoreUtils from './scoreUtils'
type ScoreInfo = {
signAfterZero: boolean
signGapTime: number
stats: { players: Stat; storage: Stat }
rate: { players: number; storage: number }
usage: { players: number; storage: number }
user: { score: number; lastSignAt: string }
}
type Stat = {
used: number
total: number
}
type SignReturn = {
score: number
storage: Stat
}
const ScoreTitle = styled.p`
@ -47,9 +43,12 @@ const ScoreNotice = styled.p`
const Dashboard: React.FC = () => {
const [loading, setLoading] = useState(false)
const [players, setPlayers] = useState<Stat>({ used: 0, total: 1 })
const [storage, setStorage] = useState<Stat>({ used: 0, total: 1 })
const [score, setScore] = useTween(0)
const [players, setPlayers] = useState(0)
const [storage, setStorage] = useState(0)
const [score, setScore] = useState(0)
const [tweenedScore, setTweenedScore] = useTween(0)
const [playersRate, setPlayersRate] = useState(1)
const [storageRate, setStorageRate] = useState(1)
const [lastSign, setLastSign] = useState(new Date())
const [canSignAfterZero, setCanSignAfterZero] = useState(false)
const [signGap, setSignGap] = useState(24)
@ -57,12 +56,13 @@ const Dashboard: React.FC = () => {
useEffect(() => {
const fetchInfo = async () => {
setLoading(true)
const { data } = await fetch.get<fetch.ResponseBody<ScoreInfo>>(
urls.user.score(),
)
setPlayers(data.stats.players)
setStorage(data.stats.storage)
const data = await fetch.get<ScoreInfo>(urls.user.score())
setPlayers(data.usage.players)
setStorage(data.usage.storage)
setTweenedScore(data.user.score)
setScore(data.user.score)
setPlayersRate(data.rate.players)
setStorageRate(data.rate.storage)
setLastSign(new Date(data.user.lastSignAt))
setCanSignAfterZero(data.signAfterZero)
setSignGap(data.signGapTime)
@ -80,10 +80,15 @@ const Dashboard: React.FC = () => {
if (code === 0) {
toast.success(message)
setLastSign(new Date())
setTweenedScore(data.score)
setScore(data.score)
setStorage(data.storage)
} else {
toast.warning(message)
const remainingTime = scoreUtils.remainingTime(
lastSign,
signGap,
canSignAfterZero,
)
toast.warning(scoreUtils.remainingTimeText(remainingTime))
}
setLoading(false)
}, [])
@ -101,17 +106,17 @@ const Dashboard: React.FC = () => {
color="teal"
icon="gamepad"
name={t('user.used.players')}
used={players.used}
total={players.total}
used={players}
unused={score / playersRate}
unit=""
/>
{storage.used > 1024 ? (
{storage > 1024 ? (
<InfoBox
color="maroon"
icon="hdd"
name={t('user.used.storage')}
used={~~(storage.used / 1024)}
total={~~(storage.total / 1024)}
used={~~(storage / 1024)}
unused={~~(score / storageRate / 1024)}
unit="MB"
/>
) : (
@ -119,8 +124,8 @@ const Dashboard: React.FC = () => {
color="maroon"
icon="hdd"
name={t('user.used.storage')}
used={storage.used}
total={storage.total}
used={storage}
unused={score / storageRate}
unit="KB"
/>
)}
@ -128,7 +133,7 @@ const Dashboard: React.FC = () => {
<div className="col-md-4 text-center">
<ScoreTitle>{t('user.cur-score')}</ScoreTitle>
<Score data-toggle="modal" data-target="#modal-score-instruction">
{~~score}
{~~tweenedScore}
</Score>
<ScoreNotice>{t('user.score-notice')}</ScoreNotice>
</div>

View File

@ -23,11 +23,11 @@ export function remainingTime(
export function remainingTimeText(remainingTime: number): string {
const time = remainingTime / ONE_MINUTE
return time < 60
? t('user.sign-remain-time', {
? t('user.signRemainingTime', {
time: ~~time,
unit: t('user.time-unit-min'),
})
: t('user.sign-remain-time', {
: t('user.signRemainingTime', {
time: ~~(time / 60),
unit: t('user.time-unit-hour'),
})

View File

@ -4,41 +4,42 @@ import * as fetch from '@/scripts/net'
import { t } from '@/scripts/i18n'
import urls from '@/scripts/urls'
import Dashboard from '@/views/user/Dashboard'
import * as scoreUtils from '@/views/user/Dashboard/scoreUtils'
jest.mock('@/scripts/net')
function scoreInfo(data = {}, user = {}, stats = {}) {
function scoreInfo(data = {}, user = {}, usage = {}) {
return {
data: {
user: { score: 600, lastSignAt: '2018-08-07 16:06:49', ...user },
stats: {
players: { used: 3, total: 15 },
storage: { used: 5, total: 20 },
...stats,
},
signAfterZero: false,
signGapTime: '24',
...data,
user: { score: 600, lastSignAt: '2018-08-07 16:06:49', ...user },
usage: {
players: 3,
storage: 5,
...usage,
},
rate: {
players: 10,
storage: 1,
},
signAfterZero: false,
signGapTime: '24',
...data,
}
}
describe('info box', () => {
it('players', async () => {
fetch.get.mockResolvedValue(
scoreInfo({}, { score: 0 }, { players: { used: 13, total: 21 } }),
)
fetch.get.mockResolvedValue(scoreInfo({}, { score: 40 }))
const { getByText } = render(<Dashboard />)
await waitFor(() => expect(fetch.get).toBeCalledTimes(1))
expect(getByText('13')).toBeInTheDocument()
expect(getByText(/21/)).toBeInTheDocument()
expect(getByText('3')).toBeInTheDocument()
expect(getByText(/7/)).toBeInTheDocument()
})
describe('storage', () => {
it('in KB', async () => {
fetch.get.mockResolvedValue(
scoreInfo({}, { score: 0 }, { storage: { used: 700, total: 800 } }),
scoreInfo({}, { score: 100 }, { storage: 700 }),
)
const { getByText } = render(<Dashboard />)
@ -50,13 +51,13 @@ describe('info box', () => {
it('in MB', async () => {
fetch.get.mockResolvedValue(
scoreInfo({}, { score: 0 }, { storage: { used: 7168, total: 10240 } }),
scoreInfo({}, { score: 3072 }, { storage: 4096 }),
)
const { getByText } = render(<Dashboard />)
await waitFor(() => expect(fetch.get).toBeCalledTimes(1))
expect(getByText('7')).toBeInTheDocument()
expect(getByText(/10/)).toBeInTheDocument()
expect(getByText('4')).toBeInTheDocument()
expect(getByText(/7/)).toBeInTheDocument()
expect(getByText(/MB/)).toBeInTheDocument()
})
})
@ -67,11 +68,11 @@ describe('sign', () => {
fetch.get.mockResolvedValue(scoreInfo())
})
it('should succeed', async () => {
it('succeeded', async () => {
fetch.post.mockResolvedValue({
code: 0,
message: 'ok',
data: { score: 900, storage: { used: 5, total: 25 } },
data: { score: 900 },
})
const { getByRole, getByText, queryByText } = render(<Dashboard />)
@ -83,18 +84,22 @@ describe('sign', () => {
expect(getByText('ok')).toBeInTheDocument()
expect(getByRole('status')).toHaveClass('alert-success')
expect(button).toBeDisabled()
expect(queryByText(/25/)).toBeInTheDocument()
expect(queryByText(/905/)).toBeInTheDocument()
})
it('should fail', async () => {
fetch.post.mockResolvedValue({ code: 1, message: 'f', data: {} })
it('failed', async () => {
fetch.post.mockResolvedValue({ code: 1, message: '' })
const { getByRole, getByText } = render(<Dashboard />)
await waitFor(() => expect(fetch.get).toBeCalledTimes(1))
fireEvent.click(getByRole('button'))
await waitFor(() => expect(fetch.post).toBeCalledWith(urls.user.sign()))
expect(getByText('f')).toBeInTheDocument()
const remainingTime = 24 * 3600 * 1000
expect(
getByText(scoreUtils.remainingTimeText(remainingTime)),
).toBeInTheDocument()
expect(getByRole('alert')).toHaveClass('alert-warning')
})
})

View File

@ -7,11 +7,6 @@ cur-score: Current Score
score-notice: Click the score to show introduction.
sign: Sign
sign-success: Signed successfully. You got :score scores.
time-unit-hour: h
time-unit-min: min
cant-sign-until: You can't sign in in :time :unit
last-sign: Last signed at :time
sign-remain-time: Available after :time :unit
announcement: Announcement
no-unread: No new notifications.

View File

@ -34,11 +34,10 @@ class UserControllerTest extends TestCase
$uid = $user->uid;
factory(\App\Models\Player::class)->create(['uid' => $uid]);
$announcement = (new Parsedown())->text(option_localized('announcement'));
$this->actingAs($user)
->get('/user')
->assertViewHas('statistics')
->assertSee((new Parsedown())->text(option_localized('announcement')), false)
->assertSee((string) $user->score);
->assertSee($announcement, false);
$filter->assertApplied('grid:user.index');
$filter->assertApplied('user_avatar', function ($url, $user) use ($uid) {
$this->assertTrue(Str::contains($url, '/avatar/user/'.$uid));
@ -60,26 +59,22 @@ class UserControllerTest extends TestCase
$this->actingAs($user)
->get('/user/score-info')
->assertJson(['data' => [
->assertJson([
'user' => [
'score' => $user->score,
'lastSignAt' => $user->last_sign_at,
],
'stats' => [
'players' => [
'used' => 1,
'total' => 11,
'percentage' => 1 / 11 * 100,
],
'storage' => [
'used' => 0,
'total' => $user->score,
'percentage' => 0,
],
'rate' => [
'storage' => (int) option('score_per_storage'),
'players' => (int) option('score_per_player'),
],
'usage' => [
'players' => 1,
'storage' => 0,
],
'signAfterZero' => option('sign_after_zero'),
'signGapTime' => option('sign_gap_time'),
]]);
]);
}
public function testSign()
@ -87,7 +82,7 @@ class UserControllerTest extends TestCase
option(['sign_score' => '50,50']);
$user = factory(User::class)->create();
// Success
// success
$this->actingAs($user)
->postJson('/user/sign')
->assertJson([
@ -95,57 +90,25 @@ class UserControllerTest extends TestCase
'message' => trans('user.sign-success', ['score' => 50]),
'data' => [
'score' => option('user_initial_score') + 50,
'storage' => [
'percentage' => 0,
'total' => option('user_initial_score') + 50,
'used' => 0,
],
'remaining_time' => (int) option('sign_gap_time'),
],
]);
// Remaining time is greater than 0
// remaining time is greater than 0
$user = factory(User::class)->create(['last_sign_at' => Carbon::now()]);
option(['sign_gap_time' => 2]);
$this->actingAs($user)
->postJson('/user/sign')
->assertJson([
'code' => 1,
'message' => trans(
'user.cant-sign-until',
[
'time' => 2,
'unit' => trans('user.time-unit-hour'),
]
),
]);
->assertJson(['code' => 1]);
// Can sign after 0 o'clock
// can sign after 0 o'clock
option(['sign_after_zero' => true]);
$user = factory(User::class)->create(['last_sign_at' => Carbon::now()]);
$diff = \Carbon\Carbon::now()->diffInSeconds(\Carbon\Carbon::tomorrow());
if ($diff / 3600 >= 1) {
$diff = round($diff / 3600);
$unit = 'hour';
} else {
$diff = round($diff / 60);
$unit = 'min';
}
$this->actingAs($user)
->postJson('/user/sign')
->assertJson([
'code' => 1,
'message' => trans(
'user.cant-sign-until',
[
'time' => $diff,
'unit' => trans("user.time-unit-$unit"),
]
),
]);
->assertJson(['code' => 1]);
$user = factory(User::class)->create([
'last_sign_at' => \Carbon\Carbon::today(),
'last_sign_at' => Carbon::today(),
]);
$this->actingAs($user)->postJson('/user/sign')->assertJson(['code' => 0]);
}