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 */ /** @var User */
$user = auth()->user(); $user = auth()->user();
return $user return $user
->makeHidden(['password', 'ip', 'remember_token', 'verification_token']); ->makeHidden(['password', 'ip', 'remember_token', 'verification_token']);
} }
@ -31,11 +32,11 @@ class UserController extends Controller
{ {
$user = Auth::user(); $user = Auth::user();
[$from, $to] = explode(',', option('sign_score')); [$min, $max] = explode(',', option('sign_score'));
$scoreIntro = trans('user.score-intro.introduction', [ $scoreIntro = trans('user.score-intro.introduction', [
'initial_score' => option('user_initial_score'), 'initial_score' => option('user_initial_score'),
'score-from' => $from, 'score-from' => $min,
'score-to' => $to, 'score-to' => $max,
'return-score' => option('return_score') 'return-score' => option('return_score')
? trans('user.score-intro.will-return-score') ? trans('user.score-intro.will-return-score')
: trans('user.score-intro.no-return-score'), : trans('user.score-intro.no-return-score'),
@ -60,10 +61,6 @@ class UserController extends Controller
$parsedown = new Parsedown(); $parsedown = new Parsedown();
return view('user.index')->with([ 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, 'score_intro' => $scoreIntro,
'rates' => [ 'rates' => [
'storage' => option('score_per_storage'), 'storage' => option('score_per_storage'),
@ -80,89 +77,55 @@ class UserController extends Controller
{ {
$user = Auth::user(); $user = Auth::user();
return json('', 0, [ return response()->json([
'user' => [ 'user' => [
'score' => $user->score, 'score' => $user->score,
'lastSignAt' => $user->last_sign_at, 'lastSignAt' => $user->last_sign_at,
], ],
'stats' => [ 'rate' => [
'players' => $this->calculatePercentageUsed($user->players->count(), option('score_per_player')), 'storage' => (int) option('score_per_storage'),
'storage' => $this->calculatePercentageUsed($this->getStorageUsed($user), 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'), 'signAfterZero' => (bool) option('sign_after_zero'),
'signGapTime' => (int) option('sign_gap_time'), '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() public function sign()
{ {
/** @var User */
$user = Auth::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->score += $acquiredScore;
$user->last_sign_at = Carbon::now(); $user->last_sign_at = Carbon::now();
$user->save(); $user->save();
$gap = option('sign_gap_time');
return json(trans('user.sign-success', ['score' => $acquiredScore]), 0, [ return json(trans('user.sign-success', ['score' => $acquiredScore]), 0, [
'score' => $user->score, 'score' => $user->score,
'storage' => $this->calculatePercentageUsed($this->getStorageUsed($user), option('score_per_storage')),
'remaining_time' => $gap > 1 ? round($gap) : $gap,
]); ]);
} else { } else {
$remaining_time = $this->getUserSignRemainingTimeWithPrecision($user); return json('', 1);
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);
} }
} }
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() public function sendVerificationEmail()
{ {
if (!option('require_verification')) { if (!option('require_verification')) {
@ -187,7 +150,6 @@ class UserController extends Controller
try { try {
Mail::to($user->email)->send(new EmailVerification(url($url))); Mail::to($user->email)->send(new EmailVerification(url($url)));
} catch (\Exception $e) { } catch (\Exception $e) {
// Write the exception to log
report($e); report($e);
return json(trans('user.verification.failed', ['msg' => $e->getMessage()]), 2); 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) public function handleProfile(Request $request, Filter $filter, Dispatcher $dispatcher)
{ {
$action = $request->input('action', ''); $action = $request->input('action', '');
/** @var User */
$user = Auth::user(); $user = Auth::user();
$addition = $request->except('action'); $addition = $request->except('action');
@ -333,6 +296,7 @@ class UserController extends Controller
{ {
$request->validate(['tid' => 'required|integer']); $request->validate(['tid' => 'required|integer']);
$tid = $request->input('tid'); $tid = $request->input('tid');
/** @var User */
$user = auth()->user(); $user = auth()->user();
$can = $filter->apply('user_can_update_avatar', true, [$user, $tid]); $can = $filter->apply('user_can_update_avatar', true, [$user, $tid]);

View File

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

View File

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

View File

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

View File

@ -7,11 +7,6 @@ cur-score: Current Score
score-notice: Click the score to show introduction. score-notice: Click the score to show introduction.
sign: Sign sign: Sign
sign-success: Signed successfully. You got :score scores. 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 announcement: Announcement
no-unread: No new notifications. no-unread: No new notifications.

View File

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