add 狗狗银行
This commit is contained in:
@@ -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 天的比赛中完全可以完成,一些选手因此误以为手工操作也是这道题的一种预期解法,不仅没有借助这道题学会编写脚本发请求,反而浪费了大量时间和精力。今后出题应当避免类似情况,如果预期解需要自动化操作,就不应当让手工操作有完成的可能性。
|
||||
|
||||
@@ -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)
|
||||
@@ -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"
|
||||
@@ -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
Executable
+37
@@ -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'])
|
||||
Reference in New Issue
Block a user