add 狗狗银行

This commit is contained in:
Hypercube
2020-11-06 14:28:54 +08:00
parent f35ff42e1b
commit 52b2a62747
18 changed files with 7103 additions and 0 deletions
+51
View File
@@ -1 +1,52 @@
# 狗狗银行
题目源代码与解题脚本见[附件](src),其中 `solution.py` 为解题脚本。
## 解法
这道题的主要考点是利用利息四舍五入的规则将实际到手利率翻倍,次要考点是编写脚本自动执行重复性操作。
利率翻倍这一步没什么好说的,简单实验一会就不难发现,当储蓄卡存有 167 狗狗币时,每日利息应当是 0.501 狗狗币,但四舍五入就是 1 狗狗币,实际上利率约等于 0.6%,超过了信用卡利率。信用卡没有这个漏洞,不会因为利息不足 0.5 狗狗币就变成 0,这是有意设计的,避免开很多张信用卡就可以实现零利息获得任意数量的钱。
一个简单的(但不是最节约操作次数的)策略是:开一张信用卡,借很多钱,分开存到很多张储蓄卡中,每张卡存恰好 167 狗狗币。这样一来,这笔钱每天净赚 0.1% 利息,逐渐就能积累到 1000 狗狗币。每隔几天,应当把所有储蓄卡中获得的利息拿走,还回信用卡,因为每张储蓄卡存恰好 167 狗狗币时赚钱最快,越多越慢,当每张储蓄卡存 200 狗狗币时会恰好无法赚钱,之后会开始亏钱。附件中的解题脚本就是按照这个思路编写的,开了 200 张储蓄卡,每天都拿走利息,37 天可以获胜。
## 怎么写脚本
以下以最新版 Chrome 浏览器为例,其他主流浏览器一般都有类似功能。提到的浏览器界面上的文字都是英文版,使用中文版的请自行猜测我在说什么。(或者干脆别看这一段了,直接去网上搜搜怎么用 Python 写爬虫发请求应该就够了。)
要想编写脚本自动操作,首先需要知道在浏览器中手工操作时会产生什么样的网络请求。如果通过脚本发送一模一样的网络请求,就能取代手工操作,产生一模一样的效果。在狗狗银行页面上按 F12 键打开“开发者工具”,在其中切换到“Network”页面,就可以看到所有浏览器发送的网络请求。去办储蓄卡,会发现每当点击“办卡”按钮时就会有一个“create”请求出现,它的 Url 是 `http://202.38.93.111:10100/api/create`。在这一行上点右键,“Copy”,“Copy as cURL”,就能复制出类似于这样的命令:
```shell
curl 'http://202.38.93.111:10100/api/create' \
-H 'Connection: keep-alive' \
-H 'Pragma: no-cache' \
-H 'Cache-Control: no-cache' \
-H 'Accept: application/json, text/plain, */*' \
-H 'DNT: 1' \
-H 'Authorization: Bearer 1:MEUCIQDG+Pwf82nVTQC07Kkmt6YkDbYo/uEsRu8f6f3HCXGQ2AIgJFlltu51Y6lNJ1sEHMviqOnoc75k1A6Qn3xoLBxM1qw=' \
-H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.183 Safari/537.36' \
-H 'Content-Type: application/json;charset=UTF-8' \
-H 'Origin: http://202.38.93.111:10100' \
-H 'Referer: http://202.38.93.111:10100/' \
-H 'Accept-Language: en-US,en;q=0.9' \
--data-binary '{"type":"debit"}' \
--compressed \
--insecure
```
在 Linux(需要安装 `curl`)命令行上运行这条命令,再刷新网页,就会发现也能办出储蓄卡。实际上,对于这道题的后端服务器来说,这个请求重要的部分只有:
- 发送 `POST` 请求给 `http://202.38.93.111:10100/api/create`
- 包含 HTTP 请求头:`Authorization: Bearer 1:MEUCIQDG+Pwf82nVTQC07Kkmt6YkDbYo/uEsRu8f6f3HCXGQ2AIgJFlltu51Y6lNJ1sEHMviqOnoc75k1A6Qn3xoLBxM1qw=`,注意其中包含着 token,这说明了你在操作哪个账号。
- 包含 HTTP 请求头:`Content-Type: application/json;charset=UTF-8`,这说明了这个请求的正文是 JSON 格式的。
- 包含请求正文:`{"type":"debit"}`
除了使用 `curl` 命令以外,还可以用各种编程语言发送这个请求,方法上网搜索很容易找到。
再经过一些尝试,不难找到办信用卡的请求、吃饭的请求、以及转账的请求(有的请求的正文中有可以改变的参数,表示卡号等信息)。这样就有了完成这道题所需要的所有请求。可以使用复制粘贴加一点修改的方法在一份文档中整理好需要按顺序执行的所有请求,然后全部执行,当然还是推荐学习一下怎么写变量和循环语句。
## 花絮
这道题的创意来自于曾经在一个群里看到有人说自己在现实中这么做,买了很多很多份理财产品来实现利率翻倍,每份几十元,每次需要用钱时取出来几份即可。平时我也常想余额宝之类的产品实际上应该怎么处理类似问题,没想到确实可以实践。
实际出题时,为了让各种数字看起来比较合理,并且解出题目所需要的操作次数不太多,还是花了不少力气凑数字的(例如:利率、饭钱、初始资金、获胜条件等)。实测用附件中提供的(并不是最节约操作次数的)解题脚本通过网络做这道题,一般都能在 10 分钟内做完,符合“预期解法不应当需要很长时间或昂贵机器”的目标。遗憾是这导致手工进行所有操作所需要的时间也较短,在 7 天的比赛中完全可以完成,一些选手因此误以为手工操作也是这道题的一种预期解法,不仅没有借助这道题学会编写脚本发请求,反而浪费了大量时间和精力。今后出题应当避免类似情况,如果预期解需要自动化操作,就不应当让手工操作有完成的可能性。
+3
View File
@@ -0,0 +1,3 @@
.stack-work/
*.cabal
/db
@@ -0,0 +1,42 @@
name: api
source-dirs: src
executables:
api:
main: api.hs
ghc-options:
- -Wall
- -Werror
- -Wcompat
- -Widentities
- -Wincomplete-record-updates
- -Wincomplete-uni-patterns
- -Wmissing-export-lists
- -Wpartial-fields
- -Wredundant-constraints
- -fhide-source-paths
- -freverse-errors
- -O2
default-extensions:
- BangPatterns
- DerivingStrategies
- GeneralizedNewtypeDeriving
- LambdaCase
- MultiWayIf
- NoImplicitPrelude
- OverloadedStrings
- ScopedTypeVariables
dependencies:
- aeson
- base
- monad-logger
- persistent
- persistent-sqlite
- rio
- wai-cors
- warp
- yesod
@@ -0,0 +1,51 @@
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
module App.Models
( Account (..)
, AccountId
, EntityField (..)
, Transaction (..)
, TransactionId
, Unique (..)
, User (..)
, UserId
, migrateAll
) where
import RIO
import Yesod
import App.Types
share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
User
token Text
date Date
nextAccountId AccountIdPerUser
UniqueUserToken token
Account
userId UserId
idPerUser AccountIdPerUser
data AccountData
nextTransactionId TransactionIdPerAccount
UniqueAccountIdPerUser userId idPerUser
Transaction
userId UserId
accountIdPerUser AccountIdPerUser
idPerAccount TransactionIdPerAccount
date Date
description Text
amount Integer
balance Integer
UniqueTransactionIdPerAccount userId accountIdPerUser idPerAccount
|]
@@ -0,0 +1,99 @@
{-# OPTIONS_GHC -Wno-orphans #-}
{-# LANGUAGE TemplateHaskell #-}
module App.Types
( AccountData
, AccountIdPerUser
, Date
, TransactionIdPerAccount
, accountBalance
, accountChange
, accountInterest
, accountType
, accountValue
, mkCredit
, mkDebit
) where
import RIO
import qualified RIO.Text as T
import Database.Persist.Sql (PersistFieldSql (sqlType))
import Yesod
instance PersistField Integer where
toPersistValue = toPersistValue . show
fromPersistValue x@(PersistText t) = case readMaybe $ T.unpack t of
Just v -> Right v
Nothing -> Left $ T.pack $ "Can not read as Integer: " <> show x
fromPersistValue (PersistInt64 i) = Right $ fromIntegral i
fromPersistValue x = Left $ T.pack $ "Can not read as Integer: " <> show x
instance PersistFieldSql Integer where
sqlType _ = SqlString
newtype Date = Date Int64
deriving stock (Show)
deriving newtype (Enum, PersistField, PersistFieldSql, ToJSON)
instance Bounded Date where
minBound = Date 1
maxBound = Date maxBound
newtype AccountIdPerUser = AccountIdPerUser Int64
deriving stock (Show)
deriving newtype (Enum, Eq, FromJSON, Num, Ord, PathPiece, PersistField, PersistFieldSql, ToJSON)
instance Bounded AccountIdPerUser where
minBound = AccountIdPerUser 1
maxBound = AccountIdPerUser maxBound
newtype TransactionIdPerAccount = TransactionIdPerAccount Int64
deriving stock (Show)
deriving newtype (Enum, Eq, Num, Ord, PersistField, PersistFieldSql, ToJSON)
instance Bounded TransactionIdPerAccount where
minBound = TransactionIdPerAccount 1
maxBound = TransactionIdPerAccount maxBound
data AccountData
= Debit !Integer
| Credit !Integer
deriving (Read, Show)
derivePersistField "AccountData"
mkDebit :: AccountData
mkDebit = Debit 0
mkCredit :: AccountData
mkCredit = Credit 0
accountValue :: AccountData -> Integer
accountValue (Debit v) = v
accountValue (Credit v) = -v
accountType :: AccountData -> Text
accountType (Debit _) = "debit"
accountType (Credit _) = "credit"
accountBalance :: AccountData -> Integer
accountBalance (Debit v) = v
accountBalance (Credit v) = v
accountChange :: Integer -> AccountData -> Either Text AccountData
accountChange d (Debit v)
| v + d >= 0 = Right $ Debit (v + d)
| otherwise = Left "余额不足"
accountChange d (Credit v)
| v - d >= 0 = Right $ Credit (v - d)
| otherwise = Left "还款不能超过欠款额"
accountInterest :: AccountData -> Integer
accountInterest (Debit v) = round $ fromInteger v * (0.003 :: Rational)
accountInterest (Credit 0) = 0
accountInterest (Credit v) = negate $ max 10 $ round $ fromInteger v * (0.005 :: Rational)
+227
View File
@@ -0,0 +1,227 @@
{-# OPTIONS_GHC -Wno-unused-top-binds #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}
module Main (main) where
import RIO hiding (Handler)
import qualified RIO.Partial as RIO'
import qualified RIO.Text as T
import Control.Monad.Logger (filterLogger, runStderrLoggingT)
import Data.Aeson.Types (Parser, parseMaybe)
import Database.Persist.Sql (ConnectionPool, SqlBackend, runMigration, runSqlPool)
import Database.Persist.Sqlite (withSqlitePool)
import Network.Wai.Handler.Warp (run)
import Network.Wai.Middleware.Cors
( CorsResourcePolicy (corsRequestHeaders)
, cors
, simpleCorsResourcePolicy
)
import Yesod
import App.Models
import App.Types
accountLimit :: AccountIdPerUser
accountLimit = 1000
transactionLimit :: TransactionIdPerAccount
transactionLimit = 100000
main :: IO ()
main = runLoggingT $ withSqlitePool "db" 1 $ \pool -> liftIO $ do
runSqlPool (runMigration migrateAll) pool
app <- toWaiApp $ Api
{ apiDatabasePool = pool
}
run 3000 $ cors getPolicy app
where
runLoggingT = runStderrLoggingT . filterLogger (\_ l -> l >= Yesod.LevelInfo)
getPolicy _ = Just $ simpleCorsResourcePolicy
{ corsRequestHeaders = ["Authorization", "Content-Type"]
}
data Api = Api
{ apiDatabasePool :: !ConnectionPool
}
mkYesod "Api" [parseRoutes|
/user UserR GET
/transactions TransactionsR GET
/reset ResetR POST
/create CreateR POST
/eat EatR POST
/transfer TransferR POST
|]
instance Yesod Api where
makeSessionBackend _ = return Nothing
yesodMiddleware = id
instance YesodPersist Api where
type YesodPersistBackend Api = SqlBackend
runDB action = do
pool <- getsYesod apiDatabasePool
runSqlPool action pool
getBody :: FromJSON a => (a -> Parser b) -> ReaderT SqlBackend Handler b
getBody parser = do
body <- requireCheckJsonBody
maybe (invalidArgs ["请求解析失败"]) pure $ parseMaybe parser body
getUser :: ReaderT SqlBackend Handler (Entity User)
getUser = do
token <- lookupBearerAuth >>= maybe notAuthenticated pure
unless (T.length token > 30) notAuthenticated
getBy (UniqueUserToken token) >>= \case
Just x -> pure x
Nothing -> do
let ai = minBound
user = User
{ userToken = token
, userDate = minBound
, userNextAccountId = RIO'.succ ai
}
ui <- insert user
void $ insert $ Account
{ accountUserId = ui
, accountIdPerUser = ai
, accountData = mkDebit
, accountNextTransactionId = minBound
}
void $ change (Entity ui user) ai "开户" 1000
pure (Entity ui user)
change :: Entity User -> AccountIdPerUser -> Text -> Integer -> ReaderT SqlBackend Handler (Entity Account)
change (Entity ui user) ai description delta = do
Entity accountId account <- getBy (UniqueAccountIdPerUser ui ai)
>>= maybe (invalidArgs ["卡不存在"]) pure
let ti = accountNextTransactionId account
when (ti > transactionLimit) $ invalidArgs ["某张卡的交易数量已达到上限"]
d <- either (invalidArgs . pure) pure
$ accountChange delta $ accountData account
void $ insert $ Transaction
{ transactionUserId = ui
, transactionAccountIdPerUser = ai
, transactionIdPerAccount = ti
, transactionDate = userDate user
, transactionDescription = description
, transactionAmount = delta
, transactionBalance = accountBalance d
}
replace accountId $ account { accountData = d, accountNextTransactionId = RIO'.succ ti }
pure $ Entity accountId account
nextDay :: Entity User -> ReaderT SqlBackend Handler ()
nextDay (Entity ui user) = do
accounts <- fmap entityVal <$> selectList [AccountUserId ==. ui] []
forM_ accounts $ \account -> do
let ai = accountIdPerUser account
d = accountData account
change (Entity ui user) ai "利息" $ accountInterest d
replace ui $ user { userDate = RIO'.succ $ userDate user }
totalValue :: [Entity Account] -> Integer
totalValue = sum . fmap (accountValue . accountData . entityVal)
getUserR :: Handler Value
getUserR = runDB $ do
Entity ui user <- getUser
accounts <- selectList [AccountUserId ==. ui] [Asc AccountIdPerUser]
flag <- if totalValue accounts >= 2000
then Just <$> makeFlag
else pure Nothing
pure $ object
[ "date" .= userDate user
, "accounts" .= (accountToJson <$> entityVal <$> accounts)
, "flag" .= flag
]
where
makeFlag = pure ("flag{W0W.So.R1ch.Much.Smart.52f2d579}" :: Text)
accountToJson x = object
[ "id" .= accountIdPerUser x
, "type" .= accountType (accountData x)
, "balance" .= accountBalance (accountData x)
]
getTransactionsR :: Handler Value
getTransactionsR = runDB $ do
ui <- entityKey <$> getUser
ai <- lookupGetParam "account" >>= \case
Just x -> maybe (invalidArgs ["参数解析失败"]) pure $ fromPathPiece x
Nothing -> invalidArgs ["参数不足"]
transactions <- selectList
[ TransactionUserId ==. ui
, TransactionAccountIdPerUser ==. ai
]
[ Asc TransactionIdPerAccount ]
pure $ object
[ "transactions" .= (transactionToJson <$> entityVal <$> transactions)
]
where
transactionToJson x = object
[ "id" .= transactionIdPerAccount x
, "date" .= transactionDate x
, "description" .= transactionDescription x
, "change" .= transactionAmount x
, "balance" .= transactionBalance x
]
postResetR :: Handler ()
postResetR = runDB $ do
ui <- entityKey <$> getUser
deleteWhere [TransactionUserId ==. ui]
deleteWhere [AccountUserId ==. ui]
delete ui
postCreateR :: Handler ()
postCreateR = runDB $ do
Entity ui user <- getUser
d <- getBody parser
let ai = userNextAccountId user
when (ai > accountLimit) $ invalidArgs ["卡数量已达到上限"]
void $ insert $ Account
{ accountUserId = ui
, accountIdPerUser = ai
, accountData = d
, accountNextTransactionId = minBound
}
void $ change (Entity ui user) ai "开户" 0
replace ui $ user { userNextAccountId = RIO'.succ ai }
where
parser x = x .: "type" >>= \case
"debit" -> pure mkDebit
"credit" -> pure mkCredit
(_ :: Text) -> fail "卡类型错误"
postEatR :: Handler ()
postEatR = runDB $ do
Entity ui user <- getUser
ai <- getBody parser
void $ change (Entity ui user) ai "吃饭" (-10)
nextDay (Entity ui user)
where
parser x = x .: "account"
postTransferR :: Handler ()
postTransferR = runDB $ do
Entity ui user <- getUser
(aiSrc, aiDst, amount) <- getBody parser
void $ change (Entity ui user) aiSrc "转账" $ negate amount
void $ change (Entity ui user) aiDst "转账" amount
where
parser x = (,,)
<$> x .: "src"
<*> x .: "dst"
<*> x .: "amount"
+1
View File
@@ -0,0 +1 @@
resolver: lts-16.5
@@ -0,0 +1,12 @@
# This file was autogenerated by Stack.
# You should not edit this file by hand.
# For more information, please see the documentation at:
# https://docs.haskellstack.org/en/stable/lock_files
packages: []
snapshots:
- completed:
size: 531707
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/16/5.yaml
sha256: 9751e25e0af5713a53ddcfcc79564b082c71b1b357fadef0d85672a5b5ba3703
original: lts-16.5
@@ -0,0 +1 @@
/dist/
@@ -0,0 +1,33 @@
{
"name": "dogebank",
"private": true,
"scripts": {
"start": "webpack-dev-server",
"build": "webpack -p",
"clean": "rm dist/*"
},
"dependencies": {
"@ant-design/icons": "^4.2.2",
"antd": "^4.5.4",
"axios": "^0.19.2",
"core-js": "^3.6.5",
"react": "^16.13.1",
"react-dom": "^16.13.1"
},
"devDependencies": {
"@babel/core": "^7.11.1",
"@babel/preset-env": "^7.11.0",
"@babel/preset-react": "^7.10.4",
"babel-loader": "^8.1.0",
"babel-plugin-import": "^1.13.0",
"css-loader": "^4.2.1",
"file-loader": "^6.0.0",
"html-webpack-plugin": "^4.3.0",
"less": "^3.12.2",
"less-loader": "^6.2.0",
"style-loader": "^1.2.1",
"webpack": "^4.44.1",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
}
}
@@ -0,0 +1,319 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import Axios from 'axios';
import { Button, Card, Descriptions, Form, InputNumber, Layout, Modal, Select, Spin, Table } from 'antd';
import './style.css';
import Coin from './coin.png';
function account_name({ type, id }) {
return {debit: '储蓄卡', credit: '信用卡'}[type] + ' ' + id;
}
function account_balance_name({ type }) {
return {debit: '余额', credit: '欠款'}[type];
}
function account_daily_interest({ type, balance }) {
if (type === 'debit') {
return Math.round(balance * 0.003);
}
if (type === 'credit') {
return balance ? Math.max(10, Math.round(balance * 0.005)) : 0;
}
}
function account_interest_text({ type }) {
return {
debit: <span>每日 <span className='amount-raw'>0.3%</span></span>,
credit: <span>每日 <span className='amount-raw'>0.5%</span>,最低 <Amount value={10} /></span>,
}[type];
}
function Amount({ value, suffix, ...props }) {
return (
<span className='amount' {...props}>
{value}
<img src={Coin} />
</span>
);
}
function Account({ account, ...props }) {
return (
<Card
className={`account account-${account.type}`}
size='small'
hoverable
title={account_name(account)}
{...props}
>
<div className='split'>
<span>{account_balance_name(account)}</span>
<Amount value={account.balance} />
</div>
</Card>
);
}
function useTransferForm({ accounts, onSubmit }) {
const [visible, setVisible] = useState(false);
const [form] = Form.useForm();
function show() {
setVisible(true);
}
function onCancel() {
setVisible(false);
}
function onOk() {
form.validateFields().then(v => {
form.resetFields();
setVisible(false);
onSubmit(v);
}).catch(e => { console.log(e); });
}
if (accounts === undefined) {
return [null, show];
}
const node = (
<Modal visible={visible} title='转账' onCancel={onCancel} onOk={onOk}>
<Form form={form}>
<Form.Item
name='src'
label='付款卡'
rules={[{ required: true, message: '请选择付款卡' }]}
>
<Select allowClear showSearch optionFilterProp='children'>
{accounts.map(i => (
<Select.Option key={i.id} value={i.id}>{account_name(i)}</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name='dst'
label='收款卡'
rules={[{ required: true, message: '请选择收款卡' }]}
>
<Select allowClear showSearch optionFilterProp='children'>
{accounts.map(i => (
<Select.Option key={i.id} value={i.id}>{account_name(i)}</Select.Option>
))}
</Select>
</Form.Item>
<Form.Item
name='amount'
label='数额'
rules={[
{ required: true, message: '请填写数额' },
{ type: 'number', min: 1, message: '数额至少为 1' },
]}
>
<InputNumber allowClear precision={0} min={0} step={100} />
</Form.Item>
</Form>
</Modal>
);
return [node, show];
}
function Content({ account, transactions, showTransferForm, newAccount, nextDay }) {
if (!account) {
return <>
<Card
title='狗狗银行储蓄卡'
extra={<Button type='primary' onClick={() => newAccount('debit')}>办卡</Button>}
style={{ margin: '24px' }}
>
<p>灵活理财就办狗狗银行储蓄卡</p>
</Card>
<Card
title='狗狗银行信用卡'
extra={<Button type='primary' onClick={() => newAccount('credit')}>办卡</Button>}
style={{ margin: '24px' }}
>
<p>无抵押无担保额度高利率低</p>
<p>无需提供身份证最快下款一秒种</p>
<p>急用钱就办狗狗银行信用卡</p>
</Card>
</>;
}
const columns = [
{ title: '日期', dataIndex: 'date', key: 'date', align: 'center' },
{ title: '类型', dataIndex: 'description', key: 'description' },
{ title: '交易额', dataIndex: 'change', key: 'change', align: 'right', render: v => <Amount value={v} /> },
{ title: account_balance_name(account), dataIndex: 'balance', key: 'balance', align: 'right', render: v => <Amount value={v} /> },
];
return <>
<Card style={{ margin: '24px' }}>
<Descriptions
column={2}
title={account_name(account)}
extra={<Button type='primary' onClick={showTransferForm}>转账</Button>}
>
<Descriptions.Item label={account_balance_name(account)}>
<Amount value={account.balance} />
</Descriptions.Item>
<Descriptions.Item label='每日利息'>
<Amount value={account_daily_interest(account)} />
</Descriptions.Item>
<Descriptions.Item label='利率' span={2}>
{account_interest_text(account)}
</Descriptions.Item>
</Descriptions>
</Card>
<Card style={{ margin: '24px' }}>
<Form layout='inline' onFinish={() => nextDay(account.id)}>
<span>用这张卡花费 <Amount value={10} /> 吃饭并结束一天</span>
<Button
type='primary'
htmlType='submit'
style={{ position: 'absolute', top: '22px', right: '22px' }}
>
确认
</Button>
</Form>
</Card>
<Card style={{ margin: '24px' }}>
{transactions ? (
<Table columns={columns} dataSource={transactions.slice().reverse()} rowKey='id' />
) : (
<Table columns={columns} loading />
)}
</Card>
</>;
}
export default function App({ token }) {
const [accounts, setAccounts] = useState(undefined);
const [flag, setFlag] = useState(null);
const [accountId, setAccountId] = useState(1);
const [transactions, setTransactions] = useState(undefined);
const axios = useMemo(() => Axios.create({
baseURL: '/api',
timeout: 10000,
headers: { Authorization: `Bearer ${token}` },
}), [token]);
const loadTransactions = useCallback(() => {
setTransactions(undefined);
if (accountId !== null) {
axios.get('/transactions', {
params: { account: accountId },
}).then(({ data: { transactions } }) => {
setTransactions(transactions);
}).catch(e => {
console.log(e);
try { alert(e.response.data.errors.join('\n')); }
catch {}
setTransactions(null);
});
}
}, [axios, accountId]);
const loadAccounts = useCallback(() => {
setAccounts(undefined);
axios.get('/user').then(({ data: { accounts, flag } }) => {
setAccounts(accounts);
if (flag) {
setFlag(flag);
}
}).catch(e => {
console.log(e);
try { alert(e.response.data.errors.join('\n')); }
catch {}
alert('加载失败');
});
loadTransactions();
}, [axios, loadTransactions]);
useEffect(loadAccounts, [loadAccounts]);
const [transferForm, showTransferForm] = useTransferForm({
accounts,
onSubmit(v) {
axios.post('/transfer', v).catch(e => {
console.log(e);
try { alert(e.response.data.errors.join('\n')); }
catch {}
}).then(loadAccounts);
},
})
if (accounts === undefined) {
return (
<div className='center' style={{ height: '100vh' }}>
<Spin size='large' />
</div>
);
}
const assets = accounts.filter(i => i.type === 'debit').map(i => i.balance).reduce((a, b) => a + b, 0);
const liabilities = accounts.filter(i => i.type === 'credit').map(i => i.balance).reduce((a, b) => a + b, 0);
const equity = assets - liabilities;
return (
<Layout style={{ alignItems: 'center' }}>
<Layout.Content>
<Layout style={{ height: '100vh', width: '100vw', maxWidth: '1000px' }}>
<Layout.Sider className='sider' theme='light' width={250}>
<Card>
<h1>狗狗银行</h1>
<div className='split'>
<span>资产</span>
<Amount value={assets} />
</div>
<div className='split'>
<span>负债</span>
<Amount value={liabilities} />
</div>
<div className='split'>
<span>净资产</span>
<Amount value={equity} />
</div>
<div style={{ textAlign: 'right' }}>
净资产高于 2000 时获胜
<Button type='primary' onClick={() => {
axios.post('/reset', {}).then(() => {
setAccountId(1);
}).catch(e => {
console.log(e);
try { alert(e.response.data.errors.join('\n')); }
catch {}
}).then(loadAccounts);
// FIXME: setAccountId 没更新 loadAccounts,导致加载错误的记录
}}>重新开始</Button>
</div>
{flag ? <div>{flag}</div> : null}
</Card>
{accounts.map(i => (
<Account key={i.id} account={i} onClick={() => setAccountId(i.id)}/>
))}
<Card className='new-account' hoverable onClick={() => setAccountId(null)}>
办新卡
</Card>
</Layout.Sider>
<Layout.Content>
<Content
account={accounts.find(i => i.id === accountId)}
transactions={transactions}
showTransferForm={showTransferForm}
newAccount={type => {
axios.post('/create', { type }).catch(e => {
console.log(e);
try { alert(e.response.data.errors.join('\n')); }
catch {}
}).then(loadAccounts);
}}
nextDay={account => {
axios.post('/eat', { account }).catch(e => {
console.log(e);
try { alert(e.response.data.errors.join('\n')); }
catch {}
}).then(loadAccounts);
}}
/>
{transferForm}
</Layout.Content>
</Layout>
</Layout.Content>
</Layout>
);
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=800, initial-scale=1">
<title>狗狗银行</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
@@ -0,0 +1,17 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './app';
let token = (history.state || {}).token;
if (!token) {
token = new URL(location.href).searchParams.get('token');
if (!token) {
alert('请使用 Hackergame 网站上的题目链接访问');
}
history.replaceState({token}, '', '/');
}
if (token) {
ReactDOM.render(<App token={token} />, document.getElementById('root'));
}
@@ -0,0 +1,47 @@
.center {
display: flex;
justify-content: center;
align-items: center;
}
.split {
display: flex;
justify-content: space-between;
align-items: center;
}
.amount,
.amount-raw {
font-size: 18px;
color: rgba(0, 0, 0, 0.85);
}
.amount {
display: inline-block;
position: relative;
margin-right: 26px;
}
.amount img {
position: absolute;
top: 2px;
right: -26px;
height: 22px;
width: 22px;
margin-left: 4px;
}
.account-debit {
background: linear-gradient(315deg, #56a5ff, #94c6ff);
}
.account-credit {
background: linear-gradient(315deg, #ff7088, #ff92a4);
}
.new-account {
text-align: center;
background: linear-gradient(315deg, #dbdbdb, #ebebeb);
}
.sider {
overflow-x: hidden;
}
.sider .ant-layout-sider-children > * {
margin: 12px;
}
@@ -0,0 +1,77 @@
const HTMLWebpackPlugin = require('html-webpack-plugin');
const babel_loader = {
loader: 'babel-loader',
options: {
presets: [
['@babel/preset-env', {
targets: 'defaults',
bugfixes: true,
useBuiltIns: 'usage',
corejs: 3,
}],
'@babel/preset-react',
],
plugins: [
['import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}],
],
},
};
const file_loader = {
loader: 'file-loader',
options: {
name: '[name].[ext]',
},
};
// https://github.com/ant-design/ant-motion/issues/44#issuecomment-620033459
const less_loader = {
loader: 'less-loader',
options: {
lessOptions: {
javascriptEnabled: true,
},
},
};
module.exports = {
entry: './src',
output: { filename: '[name].js' },
devServer: {
contentBase: 'src',
},
watchOptions: {
ignored: /node_modules/,
},
module: {
rules: [
{
test: /\.js$/,
use: [babel_loader],
exclude: /node_modules/,
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', less_loader],
},
{
test: /\.(gif|jpg|png|svg)$/,
use: [file_loader],
},
],
},
plugins: [
new HTMLWebpackPlugin({
template: 'src/index.html',
}),
],
};
File diff suppressed because it is too large Load Diff
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env python3
import requests
import sys
TOKEN = '1:MEUCI...'
HOST = 'http://202.38.93.111:10100'
if len(TOKEN) <= 30:
print('请先编辑本文件,填入你的实际 token')
session = requests.Session()
session.headers['Authorization'] = f'Bearer {TOKEN}'
def get(method, **kwargs):
r = session.get(f'{HOST}/api/{method}', params=kwargs, timeout=10)
assert r.ok
return r.json()
def post(method, **kwargs):
r = session.post(f'{HOST}/api/{method}', json=kwargs, timeout=10)
assert r.ok
post('reset')
post('create', type='credit')
for i in range(3, 20300):
print(i)
post('create', type='debit')
post('transfer', src=2, dst=i, amount=167)
for date in range(1, 37):
print('date:', date)
post('eat', account=1)
for i in range(3, 170):
post('transfer', src=i, dst=2, amount=1)
for i in range(170, 203):
post('transfer', src=i, dst=1, amount=1)
post('eat', account=1)
print(get('user')['flag'])