├── .bowerrc
├── .editorconfig
├── .env.example
├── .github
├── FUNDING.yml
└── workflows
│ ├── lint.yml
│ └── test.yml
├── .gitignore
├── .scrutinizer.yml
├── Dockerfile
├── LICENSE.md
├── README.md
├── _ide_helper.php
├── codeception.yml
├── commands
├── CrontabController.php
├── FixController.php
├── GenerateController.php
├── HelloController.php
└── InitController.php
├── composer.json
├── composer.lock
├── config
├── common.php
├── console.php
├── params.php
├── test.php
└── web.php
├── controllers
└── SiteController.php
├── core
├── EventBootstrap.php
├── actions
│ └── CreateAction.php
├── behaviors
│ └── LoggerBehavior.php
├── components
│ ├── ErrorHandler.php
│ └── ResponseHandler.php
├── exceptions
│ ├── CannotOperateException.php
│ ├── ErrorCodes.php
│ ├── FileException.php
│ ├── InternalException.php
│ ├── InvalidArgumentException.php
│ └── ThirdPartyServiceErrorException.php
├── helpers
│ ├── ArrayHelper.php
│ ├── HolidayHelper.php
│ └── SearchHelper.php
├── messages
│ └── zh-CN
│ │ ├── app.php
│ │ └── exception.php
├── models
│ ├── Account.php
│ ├── AuthClient.php
│ ├── Category.php
│ ├── CurrencyRate.php
│ ├── Record.php
│ ├── Recurrence.php
│ ├── Rule.php
│ ├── Tag.php
│ ├── Transaction.php
│ └── User.php
├── requests
│ ├── JoinRequest.php
│ ├── LoginRequest.php
│ ├── RecurrenceUpdateStatusRequest.php
│ ├── RuleUpdateStatusRequest.php
│ ├── TransactionCreateByDescRequest.php
│ └── TransactionUploadRequest.php
├── services
│ ├── AccountService.php
│ ├── AnalysisService.php
│ ├── CategoryService.php
│ ├── RecurrenceService.php
│ ├── RuleService.php
│ ├── TagService.php
│ ├── TelegramService.php
│ ├── TransactionService.php
│ ├── UploadService.php
│ └── UserService.php
├── traits
│ ├── FixDataTrait.php
│ ├── SendRequestTrait.php
│ └── ServiceTrait.php
└── types
│ ├── AccountStatus.php
│ ├── AccountType.php
│ ├── AnalysisDateType.php
│ ├── AnalysisGroupDateType.php
│ ├── AuthClientStatus.php
│ ├── AuthClientType.php
│ ├── BaseStatus.php
│ ├── BaseType.php
│ ├── ColorType.php
│ ├── CurrencyCode.php
│ ├── DirectionType.php
│ ├── RecordSource.php
│ ├── RecurrenceFrequency.php
│ ├── RecurrenceStatus.php
│ ├── ReimbursementStatus.php
│ ├── RuleStatus.php
│ ├── TelegramAction.php
│ ├── TelegramKeyword.php
│ ├── TransactionRating.php
│ ├── TransactionStatus.php
│ ├── TransactionType.php
│ └── UserStatus.php
├── docker-compose.yml
├── grumphp.yml
├── image-files
├── etc
│ └── apache2
│ │ ├── apache2.conf
│ │ └── sites-available
│ │ ├── 000-default.conf
│ │ └── default-ssl.config
├── root
│ └── .bashrc
└── usr
│ └── local
│ ├── bin
│ └── docker-entrypoint.sh
│ └── etc
│ └── php
│ └── conf.d
│ ├── base.ini
│ └── xdebug.ini
├── mail
└── layouts
│ └── html.php
├── migrations
├── m200717_082932_create_user_table.php
├── m200730_062450_create_account_table.php
├── m200730_065801_create_category_table.php
├── m200730_082622_create_currency_rate_table.php
├── m200730_085233_create_tag_table.php
├── m200812_090059_add_default_column.php
├── m200814_032606_create_rule_table.php
├── m200818_021303_update_rules_table.php
├── m200818_130329_create_transaction_table.php
├── m200818_131101_create_record_table.php
├── m200821_031033_update_category_table.php
├── m200821_031118_update_account_table.php
├── m200823_094104_create_auth_client_table.php
├── m200828_095405_update_record_table.php
├── m200830_030728_create_sort_column.php
├── m200909_030624_create_recurrence_table.php
└── m200917_064807_create_exclude_from_stats_column.php
├── modules
└── v1
│ ├── Module.php
│ └── controllers
│ ├── AccountController.php
│ ├── ActiveController.php
│ ├── AnalysisController.php
│ ├── CategoryController.php
│ ├── RecordController.php
│ ├── RecurrenceController.php
│ ├── RuleController.php
│ ├── TagController.php
│ ├── TelegramController.php
│ ├── TransactionController.php
│ └── UserController.php
├── requirements.php
├── runtime
└── .gitignore
├── tests
├── _bootstrap.php
├── _data
│ └── .gitkeep
├── _output
│ └── .gitignore
├── _support
│ ├── ApiTester.php
│ └── Helper
│ │ └── Api.php
├── api.suite.yml
└── api
│ ├── CreateUserCest.php
│ └── LoginUserCest.php
├── web
├── .htaccess
├── assets
│ └── .gitignore
├── index.php
└── uploads
│ ├── .gitignore
│ └── import-template.csv
├── yii
└── yii.bat
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory" : "vendor/bower-asset"
3 | }
4 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | end_of_line = lf
8 | insert_final_newline = true
9 | indent_style = space
10 | indent_size = 4
11 | trim_trailing_whitespace = true
12 |
13 | [*.md]
14 | trim_trailing_whitespace = false
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Framework
2 | # ---------
3 | YII_DEBUG=true
4 | YII_ENV=dev
5 | APP_URL=http://localhost:8080
6 | APP_NAME=Yii-REST-API
7 | APP_TIME_ZONE=Asia/Shanghai
8 | APP_LANGUAGE=zh-CN
9 | COOKIE_VALIDATION_KEY=
10 |
11 | # Databases
12 | # ---------
13 | MYSQL_HOST=127.0.0.1
14 | MYSQL_PORT=3306
15 | MYSQL_DATABASE=cashwarden
16 | MYSQL_USERNAME=root
17 | MYSQL_PASSWORD=root
18 |
19 | ADMIN_EMAIL=admin@gmail.com
20 | SENDER_EMAIL=no-reply@gmail.com
21 | SENDER_NAME="Yii-rest-api bot"
22 |
23 | # JWT
24 | JWT_SECRET=
25 |
26 | # graylog
27 | GRAYLOG_HOST=
28 | GRAYLOG_TAG=local
29 |
30 | # telegram
31 | TELEGRAM_TOKEN=
32 | TELEGRAM_BOT_NAME=CashwardenBot
33 |
34 | # rest token expore
35 | USER_RESET_TOKEN_EXPIRE=
36 |
37 | SEO_KEYWORDS=
38 | SEO_DESCRIPTION=
39 |
40 | GOOGLE_ANALYTICS_AU=
41 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: [forecho]
2 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 | on: [ push, pull_request ]
3 |
4 | jobs:
5 | grumphp:
6 | name: Run GrumPHP
7 | runs-on: ubuntu-latest
8 | steps:
9 | - name: Checkout
10 | uses: actions/checkout@v2
11 |
12 | - name: Setup PHP
13 | uses: shivammathur/setup-php@v2
14 | with:
15 | php-version: 7.4
16 |
17 | - name: Get composer cache directory
18 | id: composercache
19 | run: echo "::set-output name=dir::$(composer config cache-files-dir)"
20 |
21 | - name: Cache composer dependencies
22 | uses: actions/cache@v2
23 | with:
24 | path: ${{ steps.composercache.outputs.dir }}
25 | # Use composer.json for key, if composer.lock is not committed.
26 | # key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
27 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
28 | restore-keys: ${{ runner.os }}-composer-
29 |
30 | - name: Install Composer dependencies
31 | run: |
32 | composer install --no-progress --prefer-dist
33 |
34 | - name: Run GrumPHP
35 | run: ./vendor/bin/grumphp run
36 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | # GitHub Action for Yii Framework with MySQL
2 | name: Testing
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 | jobs:
9 | yii:
10 | name: Yii2 (PHP ${{ matrix.php-versions }})
11 | runs-on: ubuntu-latest
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | php-versions: [ '7.4' ]
16 | # php-versions: ['7.2', '7.3', '7.4']
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v2
20 |
21 | - name: Setup PHP, with composer and extensions
22 | uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php
23 | with:
24 | php-version: ${{ matrix.php-versions }}
25 | extensions: mbstring, intl, gd, imagick, zip, dom, mysql
26 | coverage: xdebug #optional
27 |
28 | - name: Set up MySQL
29 | uses: mirromutth/mysql-action@v1.1
30 | with:
31 | collation server: utf8mb4_unicode_ci
32 | mysql version: 5.7
33 | mysql database: cashwarden
34 | mysql root password: root
35 |
36 | # https://github.com/mirromutth/mysql-action/issues/10
37 | - name: Wait for MySQL
38 | run: |
39 | while ! mysqladmin ping --host=127.0.0.1 --password=root --silent; do
40 | sleep 1
41 | done
42 |
43 | - name: Get composer cache directory
44 | id: composercache
45 | run: echo "::set-output name=dir::$(composer config cache-files-dir)"
46 |
47 | - name: Cache composer dependencies
48 | uses: actions/cache@v2
49 | with:
50 | path: ${{ steps.composercache.outputs.dir }}
51 | # Use composer.json for key, if composer.lock is not committed.
52 | # key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }}
53 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
54 | restore-keys: ${{ runner.os }}-composer-
55 |
56 | - name: Install Composer dependencies
57 | run: |
58 | composer install --no-progress --prefer-dist --no-interaction
59 |
60 | - name: Prepare the application
61 | run: |
62 | php -r "file_exists('.env') || copy('.env.example', '.env');"
63 | php yii generate/key
64 |
65 | - name: Run Tests
66 | run: |
67 | vendor/bin/codecept build
68 | php yii migrate --interactive=0
69 | nohup php -S localhost:8080 > yii.log 2>&1 &
70 | vendor/bin/codecept run --coverage --coverage-xml=coverage.clover
71 | wget https://scrutinizer-ci.com/ocular.phar
72 | php ocular.phar code-coverage:upload --format=php-clover tests/_output/coverage.clover
73 | # bash <(curl -s https://codecov.io/bash) -t ${{ secrets.CODECOV_TOKEN }} || echo 'Codecov did not collect coverage reports'
74 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # phpstorm project files
2 | .idea
3 |
4 | # netbeans project files
5 | nbproject
6 |
7 | # zend studio for eclipse project files
8 | .buildpath
9 | .project
10 | .settings
11 |
12 | # windows thumbnail cache
13 | Thumbs.db
14 |
15 | # composer vendor dir
16 | /vendor
17 |
18 | # composer itself is not needed
19 | composer.phar
20 |
21 | # Mac DS_Store Files
22 | .DS_Store
23 |
24 | # phpunit itself is not needed
25 | phpunit.phar
26 | # local phpunit config
27 | /phpunit.xml
28 |
29 | tests/_output/*
30 | tests/_support/_generated
31 |
32 | #vagrant folder
33 | /.vagrant
34 |
35 | # env
36 | .env
37 | .env.testing
38 |
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | build:
2 | nodes:
3 | analysis:
4 | tests:
5 | override:
6 | - php-scrutinizer-run
7 | checks:
8 | php:
9 | code_rating: true
10 | remove_extra_empty_lines: true
11 | remove_php_closing_tag: true
12 | remove_trailing_whitespace: true
13 | fix_use_statements:
14 | remove_unused: true
15 | preserve_multiple: false
16 | preserve_blanklines: true
17 | order_alphabetically: true
18 | fix_php_opening_tag: true
19 | fix_linefeed: true
20 | fix_line_ending: true
21 | fix_identation_4spaces: true
22 | fix_doc_comments: true
23 | tools:
24 | external_code_coverage:
25 | timeout: 600
26 | runs: 3
27 | php_analyzer: true
28 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:7.4.3-apache
2 |
3 | ARG TIMEZONE
4 |
5 | RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list \
6 | && sed -i 's|security.debian.org/debian-security|mirrors.ustc.edu.cn/debian-security|g' /etc/apt/sources.list \
7 | && requirements="vim cron mariadb-client libwebp-dev libxpm-dev libmcrypt-dev libmcrypt4 libcurl3-dev libxml2-dev \
8 | libmemcached-dev zlib1g-dev libc6 libstdc++6 libkrb5-3 openssl debconf libfreetype6-dev libjpeg-dev libtiff-dev \
9 | libpng-dev git libmagickwand-dev ghostscript gsfonts libbz2-dev libonig-dev libzip-dev zip unzip" \
10 | && apt-get update && apt-get install -y --no-install-recommends $requirements && apt-get clean && rm -rf /var/lib/apt/lists/* \
11 | && docker-php-ext-install mysqli \
12 | pdo_mysql \
13 | gd \
14 | exif \
15 | bcmath \
16 | opcache \
17 | && docker-php-ext-enable opcache
18 |
19 | RUN ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && echo ${TIMEZONE} > /etc/timezone \
20 | && printf '[PHP]\ndate.timezone = "%s"\n', ${TIMEZONE} > /usr/local/etc/php/conf.d/tzone.ini \
21 | && "date"
22 |
23 | RUN a2enmod headers && \
24 | a2enmod rewrite
25 |
26 | # Install composer
27 | COPY --from=composer /usr/bin/composer /usr/bin/composer
28 |
29 | # Add configuration files
30 | COPY image-files/ /
31 |
32 | RUN chmod 700 \
33 | /usr/local/bin/docker-entrypoint.sh
34 |
35 | WORKDIR /srv
36 | COPY . /srv/
37 |
38 | RUN composer install --prefer-dist \
39 | && chmod 777 -R /srv/runtime \
40 | && chmod +x /srv/yii \
41 | && chmod 777 -R /srv/web/assets \
42 | && chmod 777 -R /srv/web/uploads \
43 | && chown -R www-data:www-data /srv/
44 |
45 | EXPOSE 80
46 |
47 | ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
48 |
49 | CMD ["apache2-foreground"]
50 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Yii 2 REST API Project Template
6 |
7 |
8 |
9 | Yii 2 REST API Project Template is a skeleton [Yii 2](http://www.yiiframework.com/) application best for
10 | rapidly creating small rest api projects.
11 |
12 | The template contains the basic features including user join/login api.
13 | It includes all commonly used configurations that would allow you to focus on adding new
14 | features to your application.
15 |
16 | [](https://github.com/cashwarden/api/actions)
17 | [](https://github.com/cashwarden/api/actions)
18 | [](https://scrutinizer-ci.com/g/cashwarden/api/?branch=master)
19 | [](https://scrutinizer-ci.com/g/cashwarden/api/?branch=master)
20 | [](https://packagist.org/packages/cashwarden/api)
21 | [](https://packagist.org/packages/cashwarden/api)
22 | [](https://packagist.org/packages/cashwarden/api)
23 | [](https://packagist.org/packages/cashwarden/api)
24 |
25 | REQUIREMENTS
26 | ------------
27 |
28 | The minimum requirement by this project template that your Web server supports PHP 7.2.0.
29 |
30 | INSTALLATION
31 | ------------
32 |
33 | ### Install via Composer
34 |
35 | If you do not have [Composer](http://getcomposer.org/), you may install it by following the instructions
36 | at [getcomposer.org](http://getcomposer.org/doc/00-intro.md#installation-nix).
37 |
38 | You can then install this project template using the following command:
39 |
40 | ~~~
41 | composer create-project --prefer-dist cashwarden/api
42 | cd
43 | cp .env.example .env
44 | chmod 777 -R runtime/
45 | chmod 777 -R web/uploads/
46 | php yii migrate
47 | php yii generate/key # optional
48 | ~~~
49 |
50 | Now you should be able to access the application through the following URL, assuming `rest-api` is the directory
51 | directly under the Web root.
52 |
53 | ~~~
54 | http://localhost//web/
55 | ~~~
56 |
57 | ### Install from GitHub
58 |
59 | Accessing [Use this template](https://github.com/cashwarden/api/generate) Create a new repository from yii2-rest-api
60 |
61 | ```sh
62 | git clone xxxx
63 | cd
64 | cp .env.example .env
65 | chmod 777 -R runtime/
66 | chmod 777 -R web/uploads/
67 | php yii migrate
68 | php yii generate/key # optional
69 | ```
70 |
71 | You can then access the application through the following URL:
72 |
73 | ~~~
74 | http://localhost//web/
75 | ~~~
76 |
77 |
78 | ### Install with Docker
79 |
80 | Update your vendor packages
81 |
82 | ```sh
83 | docker-compose run --rm php composer update --prefer-dist
84 | ```
85 |
86 | Run the installation triggers (creating cookie validation code)
87 |
88 | ```sh
89 | docker-compose run --rm php composer install
90 | ```
91 |
92 | Start the container
93 |
94 | ```sh
95 | docker-compose up -d
96 | ```
97 |
98 | You can then access the application through the following URL:
99 |
100 | ```
101 | http://127.0.0.1:8000
102 | ```
103 |
104 | **NOTES:**
105 | - Minimum required Docker engine version `17.04` for development (see [Performance tuning for volume mounts](https://docs.docker.com/docker-for-mac/osxfs-caching/))
106 | - The default configuration uses a host-volume in your home directory `.docker-composer` for composer caches
107 |
108 | Check out the packages
109 | ------------
110 |
111 | - [yiithings/yii2-doten](https://github.com/yiithings/yii2-doten)
112 | - [sizeg/yii2-jwt](https://github.com/sizeg/yii2-jwt)
113 | - [yiier/yii2-helpers](https://github.com/yiier/yii2-helpers)
114 |
115 | Use
116 | ------------
117 |
118 | At this time, you have a RESTful API server running at `http://127.0.0.1:8000`. It provides the following endpoints:
119 |
120 | * `GET /health-check`: a health check service provided for health checking purpose (needed when implementing a server cluster)
121 | * `POST /v1/join`: create a user
122 | * `POST /v1/login`: authenticates a user and generates a JWT
123 | * `POST /v1/refresh-token`: refresh a JWT
124 |
125 | Try the URL `http://localhost:8000/health-check` in a browser, and you should see something like `{"code":0,"data":"OK","message":"成功"}` displayed.
126 |
127 | If you have `cURL` or some API client tools (e.g. [Postman](https://www.getpostman.com/)), you may try the following
128 | more complex scenarios:
129 |
130 | ```shell
131 | # create a user via: POST /v1/join
132 | curl -X POST -H "Content-Type: application/json" -d '{"username":"demo","email":"demo@email.com","password":"pass123"}' http://localhost:8000/v1/join
133 | # should return like: {"code":0,"data":{"username":"demo","email":"demo@email.com","status":1,"created_at":"2020-07-18T16:38:11+08:00","updated_at":"2020-07-18T16:38:11+08:00","id":17},"message":"成功"}
134 |
135 | # authenticate the user via: POST /v1/login
136 | curl -X POST -H "Content-Type: application/json" -d '{"username": "demo", "password": "pass123"}' http://localhost:8000/v1/login
137 | # should return like: {"code":0,"data":{"user":{"id":4,"username":"dem211o1","avatar":"","email":"de21mo1@mail.com","status":1,"created_at":"2020-07-17T23:49:39+08:00","updated_at":"2020-07-17T23:49:39+08:00"},"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IllpaS1SRVNULUFQSSJ9.eyJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3QiLCJqdGkiOiJZaWktUkVTVC1BUEkiLCJpYXQiOjE1OTUwNjQ5NzIsImV4cCI6MTU5NTMyNDE3MiwidXNlcm5hbWUiOiJkZW0yMTFvMSIsImlkIjo0fQ.y2NSVQe-TQ08RnXnF-o55h905G9WHo6GYHNaUWlKjDE"},"message":"成功"}
138 |
139 | # refresh a JWT
140 | curl -X POST -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IllpaS1SRVNULUFQSSJ9.eyJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3QiLCJqdGkiOiJZaWktUkVTVC1BUEkiLCJpYXQiOjE1OTUwNjQ5NzIsImV4cCI6MTU5NTMyNDE3MiwidXNlcm5hbWUiOiJkZW0yMTFvMSIsImlkIjo0fQ.y2NSVQe-TQ08RnXnF-o55h905G9WHo6GYHNaUWlKjDE' http://localhost:8000/v1/refresh-token
141 | # should return like: {"code":0,"data":{"user":{"id":4,"username":"dem211o1","avatar":"","email":"de21mo1@mail.com","status":1,"created_at":"2020-07-17T23:49:39+08:00","updated_at":"2020-07-17T23:49:39+08:00"},"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImp0aSI6IllpaS1SRVNULUFQSSJ9.eyJpc3MiOiJodHRwOlwvXC9sb2NhbGhvc3QiLCJqdGkiOiJZaWktUkVTVC1BUEkiLCJpYXQiOjE1OTUwNjQ5NzIsImV4cCI6MTU5NTMyNDE3MiwidXNlcm5hbWUiOiJkZW0yMTFvMSIsImlkIjo0fQ.y2NSVQe-TQ08RnXnF-o55h905G9WHo6GYHNaUWlKjDE"},"message":"成功"}
142 | ```
143 |
--------------------------------------------------------------------------------
/_ide_helper.php:
--------------------------------------------------------------------------------
1 | where(['status' => RecurrenceStatus::ACTIVE])
33 | ->andWhere(['execution_date' => Yii::$app->formatter->asDatetime('now', 'php:Y-m-d')])
34 | ->asArray()
35 | ->all();
36 | $transaction = Yii::$app->db->beginTransaction();
37 | try {
38 | foreach ($items as $item) {
39 | \Yii::$app->user->setIdentity(User::findOne($item['user_id']));
40 | if ($t = $this->transactionService->copy($item['transaction_id'], $item['user_id'])) {
41 | $keyboard = $this->telegramService->getRecordMarkup($t);
42 | $text = $this->telegramService->getMessageTextByTransaction($t, '定时记账成功');
43 | $this->telegramService->sendMessage($text, $keyboard);
44 | $this->stdout("定时记账成功,transaction_id:{$t->id}\n");
45 | }
46 | }
47 | RecurrenceService::updateAllExecutionDate();
48 | $transaction->commit();
49 | } catch (\Exception $e) {
50 | $this->stdout("定时记账失败:{$e->getMessage()}\n");
51 | $transaction->rollBack();
52 | throw $e;
53 | }
54 | }
55 |
56 | /**
57 | * @param string $type
58 | * @throws Exception
59 | */
60 | public function actionReport(string $type = AnalysisDateType::YESTERDAY)
61 | {
62 | $items = AuthClient::find()
63 | ->where(['type' => AuthClientType::TELEGRAM])
64 | ->asArray()
65 | ->all();
66 | $transaction = Yii::$app->db->beginTransaction();
67 | try {
68 | foreach ($items as $item) {
69 | $this->telegramService->sendReport($item['user_id'], $type);
70 | $this->stdout("定时发送报告成功,user_id:{$item['user_id']}\n");
71 | }
72 | $transaction->commit();
73 | } catch (\Exception $e) {
74 | $this->stdout("定时发送报告失败:{$e->getMessage()}\n");
75 | $transaction->rollBack();
76 | throw $e;
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/commands/FixController.php:
--------------------------------------------------------------------------------
1 | stdout("{$item} key [{$key}] set successfully.\n", Console::FG_GREEN);
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/commands/HelloController.php:
--------------------------------------------------------------------------------
1 |
21 | * @since 2.0
22 | */
23 | class HelloController extends Controller
24 | {
25 | /**
26 | * This command echoes what you have entered as the message.
27 | * @param string $message the message to be echoed.
28 | * @return int Exit code
29 | */
30 | public function actionIndex($message = 'hello world')
31 | {
32 | echo $message . "\n";
33 |
34 | \Yii::error(['request_id' => \Yii::$app->requestId->id, 'test_request_id']);
35 | return ExitCode::OK;
36 | }
37 |
38 | public function actionTelegram()
39 | {
40 | $client = TelegramService::newClient();
41 | dump($client->getMe());
42 | return ExitCode::OK;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/commands/InitController.php:
--------------------------------------------------------------------------------
1 | setWebHook($url);
20 | $this->stdout("Telegram set Webhook url success!: {$url}\n");
21 | return ExitCode::OK;
22 | }
23 |
24 | /**
25 | * @param int $userId
26 | * @throws \app\core\exceptions\InvalidArgumentException
27 | * @throws \yii\db\Exception
28 | */
29 | public function actionUserData(int $userId)
30 | {
31 | $this->userService->createUserAfterInitData(User::findOne($userId));
32 | $this->stdout("User Account and Category init success! \n");
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "cashwarden/api",
3 | "description": "CashWarden API",
4 | "keywords": [
5 | "yii2",
6 | "framework",
7 | "rest",
8 | "basic",
9 | "project template"
10 | ],
11 | "type": "project",
12 | "license": "BSD-3-Clause",
13 | "support": {
14 | "issues": "https://github.com/forecho/yii2-rest-api/issues?state=open",
15 | "source": "https://github.com/forecho/yii2-rest-api"
16 | },
17 | "minimum-stability": "stable",
18 | "require": {
19 | "php": ">=7.4",
20 | "yiisoft/yii2": "~2.0.14",
21 | "yiisoft/yii2-swiftmailer": "~2.0.0 || ~2.1.0",
22 | "yiithings/yii2-dotenv": "^1.0",
23 | "sizeg/yii2-jwt": "^2.0",
24 | "yiier/yii2-helpers": "^2.0",
25 | "yiier/yii2-graylog-target": "^1.1",
26 | "yiier/yii2-validators": "^0.3.0",
27 | "mis/yii2-ide-helper": "^1.0",
28 | "telegram-bot/api": "^2.3",
29 | "ext-json": "*",
30 | "ext-bcmath": "*",
31 | "guzzlehttp/guzzle": "^7.0"
32 | },
33 | "require-dev": {
34 | "yiisoft/yii2-debug": "~2.1.0",
35 | "yiisoft/yii2-gii": "~2.1.0",
36 | "yiisoft/yii2-faker": "~2.0.0",
37 | "codeception/codeception": "^4.0",
38 | "codeception/verify": "~0.5.0 || ~1.1.0",
39 | "codeception/specify": "~0.4.6",
40 | "symfony/browser-kit": ">=2.7 <=4.2.4",
41 | "codeception/module-filesystem": "^1.0.0",
42 | "codeception/module-yii2": "^1.0.0",
43 | "codeception/module-asserts": "^1.0.0",
44 | "codeception/module-rest": "^1.0.0",
45 | "codeception/module-phpbrowser": "^1.0.0",
46 | "squizlabs/php_codesniffer": "^3.5.5",
47 | "phpro/grumphp": "^0.19.1"
48 | },
49 | "config": {
50 | "process-timeout": 1800,
51 | "fxp-asset": {
52 | "enabled": false
53 | },
54 | "allow-plugins": {
55 | "yiisoft/yii2-composer": true,
56 | "phpro/grumphp": true
57 | }
58 | },
59 | "autoload": {
60 | "files": [
61 | "vendor/yiier/yii2-helpers/src/GlobalFunctions.php",
62 | "vendor/yiier/yii2-helpers/src/SupportFunctions.php"
63 | ]
64 | },
65 | "scripts": {
66 | "post-install-cmd": [
67 | "yii\\composer\\Installer::postInstall"
68 | ],
69 | "post-create-project-cmd": [
70 | "yii\\composer\\Installer::postCreateProject",
71 | "yii\\composer\\Installer::postInstall"
72 | ]
73 | },
74 | "extra": {
75 | "yii\\composer\\Installer::postCreateProject": {
76 | "setPermission": [
77 | {
78 | "runtime": "0777",
79 | "web/assets": "0777",
80 | "yii": "0755"
81 | }
82 | ]
83 | },
84 | "yii\\composer\\Installer::postInstall": {
85 | "generateCookieValidationKey": [
86 | "config/web.php"
87 | ]
88 | }
89 | },
90 | "repositories": [
91 | {
92 | "type": "composer",
93 | "url": "https://asset-packagist.org"
94 | }
95 | ]
96 | }
97 |
--------------------------------------------------------------------------------
/config/console.php:
--------------------------------------------------------------------------------
1 | 'basic-console',
8 | 'basePath' => dirname(__DIR__),
9 | 'controllerNamespace' => 'app\commands',
10 | 'aliases' => [
11 | '@bower' => '@vendor/bower-asset',
12 | '@npm' => '@vendor/npm-asset',
13 | '@tests' => '@app/tests',
14 | ],
15 | 'components' => [
16 | 'user' => [
17 | 'class' => 'yii\web\User',
18 | 'identityClass' => 'app\models\User',
19 | 'enableSession' => false,
20 | 'enableAutoLogin' => false,
21 | ],
22 | 'urlManager' => [
23 | 'baseUrl' => env('APP_URL'),
24 | 'hostInfo' => env('APP_URL')
25 | ],
26 | ],
27 | 'params' => $params,
28 | ];
29 |
30 | if (YII_ENV_DEV) {
31 | // configuration adjustments for 'dev' environment
32 | $config['bootstrap'][] = 'gii';
33 | $config['modules']['gii'] = [
34 | 'class' => 'yii\gii\Module',
35 | ];
36 | }
37 |
38 | return \yii\helpers\ArrayHelper::merge($common, $config);
39 |
--------------------------------------------------------------------------------
/config/params.php:
--------------------------------------------------------------------------------
1 | env('APP_URL'),
5 | 'adminEmail' => env('ADMIN_EMAIL'),
6 | 'senderEmail' => env('SENDER_EMAIL'),
7 | 'senderName' => env('SENDER_NAME', env('APP_NAME')),
8 | 'telegramToken' => env('TELEGRAM_TOKEN'),
9 | 'telegramBotName' => env('TELEGRAM_BOT_NAME'),
10 | 'user.passwordResetTokenExpire' => env('USER_RESET_TOKEN_EXPIRE', 3600),
11 | 'seoKeywords' => env('SEO_KEYWORDS'),
12 | 'seoDescription' => env('SEO_DESCRIPTION'),
13 | 'googleAnalyticsAU' => env('GOOGLE_ANALYTICS_AU'),
14 | // 不记录 header 指定 key 的值到日志,默认值为 authorization,配置自定义会覆盖默认值
15 | 'logFilterIgnoredHeaderKeys' => env('LOG_FILTER_IGNORED_HEADER_KEYS', 'authorization,token,cookie'),
16 | 'logFilterIgnoredKeys' => env('LOG_FILTER_IGNORED_KEYS', 'password'), // 不记录日志
17 | 'logFilterHideKeys' => env('LOG_FILTER_HIDE_KEYS'), // 用*代替所有数据
18 | 'logFilterHalfHideKeys' => env('LOG_FILTER_HALF_HIDE_KEYS', 'email'), // 部分数据隐藏,只显示头部 20% 和尾部 20% 数据,剩下的用*代替
19 | 'uploadSavePath' => '@webroot/uploads',
20 | 'uploadWebPath' => '@web/uploads',
21 | ];
22 |
--------------------------------------------------------------------------------
/config/test.php:
--------------------------------------------------------------------------------
1 | 'basic-tests',
12 | ], $web);
13 |
14 | return ArrayHelper::merge($common, $config);
15 |
--------------------------------------------------------------------------------
/config/web.php:
--------------------------------------------------------------------------------
1 | 'basic',
11 | 'basePath' => dirname(__DIR__),
12 | 'aliases' => [
13 | '@bower' => '@vendor/bower-asset',
14 | '@npm' => '@vendor/npm-asset',
15 | ],
16 | 'modules' => [
17 | 'v1' => [
18 | 'class' => 'app\modules\v1\Module',
19 | ],
20 | ],
21 | 'components' => [
22 | 'request' => [
23 | 'parsers' => [
24 | 'application/json' => 'yii\web\JsonParser',
25 | ],
26 | // !!! insert a secret key in the following (if it is empty) - this is required by cookie validation
27 | 'cookieValidationKey' => env('COOKIE_VALIDATION_KEY')
28 | ],
29 | 'response' => [
30 | 'class' => 'yii\web\Response',
31 | 'on beforeSend' => function ($event) {
32 | yii::createObject([
33 | 'class' => ResponseHandler::class,
34 | 'event' => $event,
35 | ])->formatResponse();
36 | },
37 | ],
38 | 'jwt' => [
39 | 'class' => \sizeg\jwt\Jwt::class,
40 | 'key' => env('JWT_SECRET'),
41 | ],
42 | 'user' => [
43 | 'identityClass' => User::class,
44 | 'enableAutoLogin' => true,
45 | ],
46 | 'errorHandler' => [
47 | // 'class' => '\yii\web\ErrorHandler',
48 | 'errorAction' => 'site/error',
49 | ],
50 | 'mailer' => [
51 | 'class' => 'yii\swiftmailer\Mailer',
52 | // send all mails to a file by default. You have to set
53 | // 'useFileTransport' to false and configure a transport
54 | // for the mailer to send real emails.
55 | 'useFileTransport' => true,
56 | ],
57 | 'urlManager' => [
58 | 'enablePrettyUrl' => true,
59 | 'showScriptName' => false,
60 | 'hostInfo' => getenv('APP_URL'),
61 | 'rules' => [
62 | "POST /" => '/user/',
63 | "POST /token/refresh" => '/user/refresh-token',
64 | "POST /transactions/by-description" => '/transaction/create-by-description',
65 | "POST /rules//copy" => '/rule/copy',
66 | "PUT /rules//status" => '/rule/update-status',
67 | "GET /accounts/types" => '/account/types',
68 | "GET /accounts/overview" => '/account/overview',
69 | "POST /reset-token" => '/user/reset-token',
70 | "GET /users/auth-clients" => '/user/get-auth-clients',
71 | "GET /transactions/" => '/transaction/',
72 | "POST /transactions/upload" => '/transaction/upload',
73 | "GET /records/overview" => '/record/overview',
74 | "GET /categories/analysis" => '/category/analysis',
75 | "GET /records/analysis" => '/record/analysis',
76 | "GET /records/sources" => '/record/sources',
77 | "PUT /recurrences//status" => '/recurrence/update-status',
78 | "GET /recurrences/frequencies" => '/recurrence/frequency-types',
79 |
80 | "GET /site-config" => '/site/data',
81 | "GET /" => '/site/',
82 | "GET health-check" => 'site/health-check',
83 | [
84 | 'class' => 'yii\rest\UrlRule',
85 | 'controller' => [
86 | 'v1/account',
87 | 'v1/category',
88 | 'v1/rule',
89 | 'v1/tag',
90 | 'v1/record',
91 | 'v1/transaction',
92 | 'v1/recurrence',
93 | ]
94 | ],
95 | '///' => '//',
96 | ],
97 | ],
98 | ],
99 | 'params' => $params,
100 | ];
101 |
102 | if (YII_ENV_DEV) {
103 | // configuration adjustments for 'dev' environment
104 | $config['bootstrap'][] = 'debug';
105 | $config['modules']['debug'] = [
106 | 'class' => 'yii\debug\Module',
107 | // uncomment the following to add your IP if you are not connecting from localhost.
108 | 'allowedIPs' => ['*'],
109 | ];
110 |
111 | $config['bootstrap'][] = 'gii';
112 | $config['modules']['gii'] = [
113 | 'class' => 'yii\gii\Module',
114 | // uncomment the following to add your IP if you are not connecting from localhost.
115 | 'allowedIPs' => ['*'],
116 | ];
117 | }
118 |
119 | return \yii\helpers\ArrayHelper::merge($common, $config);
120 |
--------------------------------------------------------------------------------
/controllers/SiteController.php:
--------------------------------------------------------------------------------
1 | errorHandler->exception;
34 | if ($exception !== null) {
35 | Yii::error([
36 | 'request_id' => Yii::$app->requestId->id,
37 | 'exception' => $exception->getMessage(),
38 | 'line' => $exception->getLine(),
39 | 'file' => $exception->getFile(),
40 | ], 'response_data_error');
41 |
42 | return ['code' => $exception->getCode(), 'message' => $exception->getMessage()];
43 | }
44 | return [];
45 | }
46 |
47 |
48 | public function actionIcons()
49 | {
50 | return [
51 | 'food',
52 | 'home',
53 | 'bus',
54 | 'game',
55 | 'medicine-chest',
56 | 'clothes',
57 | 'education',
58 | 'investment',
59 | 'baby',
60 | 'expenses',
61 | 'work',
62 | 'income',
63 | 'transfer',
64 | 'adjust',
65 | ];
66 | }
67 |
68 | public function actionData()
69 | {
70 | return [
71 | 'app' => [
72 | 'name' => Yii::$app->name,
73 | 'description' => params('seoDescription'),
74 | 'keywords' => params('seoKeywords'),
75 | 'google_analytics' => params('googleAnalyticsAU'),
76 | 'telegram_bot_name' => params('telegramBotName')
77 | ],
78 | 'menu' => [
79 | [
80 | 'text' => 'Main',
81 | 'group' => false,
82 | 'children' => [
83 | [
84 | 'text' => '仪表盘',
85 | 'link' => '/dashboard',
86 | 'icon' => 'anticon-dashboard',
87 | ],
88 | [
89 | 'text' => '账户',
90 | 'link' => '/account/index',
91 | 'icon' => 'anticon-account-book',
92 | ],
93 | [
94 | 'text' => '记录',
95 | 'link' => '/record/index',
96 | 'icon' => 'anticon-database',
97 | ],
98 | [
99 | 'text' => '定时',
100 | 'link' => '/recurrence/index',
101 | 'icon' => 'anticon-field-time',
102 | ],
103 | [
104 | 'text' => '分析',
105 | 'link' => '/analysis/index',
106 | 'icon' => 'anticon-area-chart',
107 | ],
108 | [
109 | 'text' => '设置',
110 | 'icon' => 'anticon-setting',
111 | 'children' => [
112 | [
113 | 'text' => '个人设置',
114 | 'link' => '/settings/personal',
115 | 'icon' => 'anticon-user',
116 | ],
117 | [
118 | 'text' => '分类设置',
119 | 'link' => '/settings/categories',
120 | 'icon' => 'anticon-appstore',
121 | ],
122 | [
123 | 'text' => '标签设置',
124 | 'link' => '/settings/tags',
125 | 'icon' => 'anticon-appstore',
126 | ],
127 | [
128 | 'text' => '规则设置',
129 | 'link' => '/settings/rules',
130 | 'icon' => 'anticon-group',
131 | ]
132 | ]
133 | ],
134 | ]
135 | ]
136 | ]
137 | ];
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/core/EventBootstrap.php:
--------------------------------------------------------------------------------
1 | beforeSend($event);
17 | });
18 |
19 | Event::on(Controller::class, Controller::EVENT_BEFORE_ACTION, function ($event) {
20 | \Yii::createObject(LoggerBehavior::class)->beforeAction();
21 | });
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/core/actions/CreateAction.php:
--------------------------------------------------------------------------------
1 | checkAccess) {
28 | call_user_func($this->checkAccess, $this->id);
29 | }
30 |
31 | /* @var $model \yii\db\ActiveRecord */
32 | $model = new $this->modelClass([
33 | 'scenario' => $this->scenario,
34 | ]);
35 |
36 | $model->load(Yii::$app->getRequest()->getBodyParams(), '');
37 | if ($model->save()) {
38 | $response = Yii::$app->getResponse();
39 | $response->setStatusCode(200);
40 | $id = implode(',', array_values($model->getPrimaryKey(true)));
41 | return $model->findOne($id);
42 | } elseif (!$model->hasErrors()) {
43 | throw new ServerErrorHttpException('Failed to create the object for unknown reason.');
44 | }
45 |
46 | return $model;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/core/behaviors/LoggerBehavior.php:
--------------------------------------------------------------------------------
1 | 'beforeSend',
18 | Controller::EVENT_BEFORE_ACTION => 'beforeAction'
19 | ];
20 | }
21 |
22 | /**
23 | * @param $event
24 | * @throws \Exception
25 | */
26 | public function beforeSend($event)
27 | {
28 | $response = $event->sender;
29 | if ($response->format != 'html') {
30 | $request = \Yii::$app->request;
31 | $params = Yii::$app->params;
32 | $requestId = Yii::$app->requestId->id;
33 | $code = ArrayHelper::getValue($response->data, 'code');
34 |
35 | $ignoredKeys = explode(',', ArrayHelper::getValue($params, 'logFilterIgnoredKeys', ''));
36 | $hideKeys = explode(',', ArrayHelper::getValue($params, 'logFilterHideKeys', ''));
37 | $halfHideKeys = explode(',', ArrayHelper::getValue($params, 'logFilterHalfHideKeys', ''));
38 | $ignoredHeaderKeys = explode(',', ArrayHelper::getValue($params, 'logFilterIgnoredHeaderKeys', ''));
39 | $requestHeaderParams = $this->headerFilter($request->headers->toArray(), $ignoredHeaderKeys);
40 |
41 | $requestParams = $this->paramsFilter($request->bodyParams, $ignoredKeys, $hideKeys, $halfHideKeys);
42 |
43 | $message = [
44 | 'request_id' => $requestId,
45 | 'type' => $code === 0 ? 'response_data_success' : 'response_data_error',
46 | 'header' => Json::encode($requestHeaderParams),
47 | 'params' => Json::encode($requestParams),
48 | 'url' => $request->absoluteUrl,
49 | 'response' => Json::encode($response->data)
50 | ];
51 | if (is_array($response->data)) {
52 | $response->data = ['request_id' => $requestId] + $response->data;
53 | }
54 | $code === 0 ? Yii::info($message, 'request') : Yii::error($message, 'request');
55 | }
56 | }
57 |
58 | public function beforeAction()
59 | {
60 | return Yii::$app->requestId->id;
61 | }
62 |
63 |
64 | /**
65 | * @param array $params
66 | * @param array $ignoredHeaderKeys
67 | * @return array
68 | */
69 | protected function headerFilter(array $params, array $ignoredHeaderKeys)
70 | {
71 | foreach ($params as $key => $item) {
72 | if ($key && in_array($key, $ignoredHeaderKeys)) {
73 | unset($params[$key]);
74 | }
75 | }
76 | return $params;
77 | }
78 |
79 | /**
80 | * @param $params array
81 | * @param array $ignoredKeys
82 | * @param array $hideKeys
83 | * @param array $halfHideKeys
84 | * @return string|int|array
85 | */
86 | protected function paramsFilter(array $params, array $ignoredKeys, array $hideKeys, array $halfHideKeys)
87 | {
88 | if (!$hideKeys && !$halfHideKeys && !$ignoredKeys) {
89 | return $params;
90 | }
91 | foreach ($params as $key => &$item) {
92 | if (is_array($item)) {
93 | $item = $this->paramsFilter($item, $ignoredKeys, $hideKeys, $halfHideKeys);
94 | continue;
95 | }
96 | if ($key && in_array($key, $ignoredKeys)) {
97 | unset($params[$key]);
98 | } elseif ($key && in_array($key, $hideKeys)) {
99 | $item = $this->paramReplace($item);
100 | } elseif ($key && in_array($key, $halfHideKeys)) {
101 | $item = $this->paramPartialHiddenReplace($item);
102 | }
103 | }
104 | return $params;
105 | }
106 |
107 | /**
108 | * @param string $value
109 | * @return string
110 | */
111 | protected function paramReplace(string $value)
112 | {
113 | return str_repeat('*', strlen($value));
114 | }
115 |
116 |
117 | /**
118 | * @param $value
119 | * @return string
120 | */
121 | protected function paramPartialHiddenReplace(string $value)
122 | {
123 | $valueLength = strlen($value);
124 | if ($valueLength > 2) {
125 | $showLength = ceil($valueLength * 0.2);
126 | $hideLength = $valueLength - $showLength * 2;
127 | $newValue = mb_substr($value, 0, $showLength)
128 | . str_repeat('*', $hideLength)
129 | . mb_substr($value, -1 * $showLength);
130 | } else {
131 | $newValue = $this->paramReplace($value);
132 | }
133 |
134 | return $newValue;
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/core/components/ErrorHandler.php:
--------------------------------------------------------------------------------
1 | event->sender;
18 | if ($response->data !== null) {
19 | if (isset($response->data['code']) && isset($response->data['message'])) {
20 | $response->data = [
21 | 'code' => $response->data['code'] ?: $response->statusCode,
22 | 'data' => isset($response->data['data']) ? $response->data['data'] : null,
23 | 'message' => $response->data['message'],
24 | ];
25 | } elseif ($response->format != 'html' && !isset($response->data['message'])) {
26 | $response->data = [
27 | 'code' => 0,
28 | 'data' => $response->data,
29 | 'message' => $this->successMessage ?: \Yii::t('app', 'Success Message'),
30 | ];
31 | } elseif ((!empty($response->data['message'])) && !isset($response->data['code'])) {
32 | $message = $response->data['message'];
33 | unset($response->data['message']);
34 | $response->data = [
35 | 'code' => 0,
36 | 'data' => isset($response->data[0]) ? $response->data[0] : $response->data,
37 | 'message' => $message,
38 | ];
39 | }
40 | }
41 | $this->formatHttpStatusCode($response);
42 | }
43 |
44 | public function formatHttpStatusCode(Response $response)
45 | {
46 | switch ($response->statusCode) {
47 | case 404:
48 | $response->data['code'] = 404;
49 | break;
50 | case 204:
51 | if (\Yii::$app->request->isDelete) {
52 | $response->data['code'] = 0;
53 | $response->data['data'] = null;
54 | $response->data['message'] = $this->successMessage ?: \Yii::t('app', 'Success Message');
55 | }
56 | break;
57 | case 422:
58 | $response->data['code'] = 422;
59 | $response->data['message'] = current($response->data['data'])['message'];
60 | break;
61 | default:
62 | # code...
63 | break;
64 | }
65 |
66 | $response->setStatusCode(200);
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/core/exceptions/CannotOperateException.php:
--------------------------------------------------------------------------------
1 | sendRequest('GET', $baseUrl);
26 | $data = json_decode($response);
27 | if ($data->code == 0) {
28 | return $data->workday->date;
29 | }
30 | } catch (GuzzleException $e) {
31 | Log::error('holiday error', [$response ?? [], (string)$e]);
32 | throw new ThirdPartyServiceErrorException();
33 | } catch (\Exception $e) {
34 | Log::error('holiday error', [$response ?? [], (string)$e]);
35 | throw new ThirdPartyServiceErrorException();
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/core/helpers/SearchHelper.php:
--------------------------------------------------------------------------------
1 | '成功',
5 | 'Incorrect username or password.' => '账号或者密码错误。',
6 | '{attribute} can only be numbers and letters.' => '{attribute}只能为数字和字母。',
7 | 'Username' => '用户名',
8 | 'Email' => '邮箱',
9 | 'Password' => '密码',
10 | 'The JWT secret must be configured first.' => '必须先配置 JWT_SECRET',
11 |
12 | 'General Account' => '一般账户',
13 | 'Cash Account' => '现金账户',
14 | 'Debit Card' => '借记卡',
15 | 'Credit Card' => '信用卡',
16 | 'Saving Account' => '储蓄账户',
17 | 'Investment Account' => '投资账户',
18 | 'Expense' => '支出',
19 | 'Income' => '收入',
20 | 'Transfer' => '转账',
21 | 'Adjust Balance' => '调整余额',
22 | 'Adjust' => '调整',
23 | # 'Cash' => '',
24 | 'Must' => '必须',
25 | 'Want' => '享乐',
26 | 'Need' => '可选',
27 |
28 | 'Current month' => '本月',
29 | 'Last month' => '上个月',
30 | 'Yesterday' => '昨天',
31 | 'Today' => '今天',
32 | 'Grand total' => '累计',
33 |
34 |
35 | 'Base Currency Code' => '基础货币',
36 | 'Food and drink' => '餐饮食品',
37 | 'Home life' => '居家生活',
38 | 'Traffic' => '行车交通',
39 | 'Recreation' => '休闲娱乐',
40 | 'Health care' => '健康医疗',
41 | 'Clothes' => '衣服饰品',
42 | 'Cultural education' => '文化教育',
43 | 'Investment expenditure' => '投资支出',
44 | 'Childcare' => '育儿',
45 | 'Other expenses' => '其他支出',
46 | 'Work income' => '工作收入',
47 | 'Investment income' => '投资收入',
48 | 'Other income' => '其他收入',
49 |
50 |
51 | 'The {attribute} has been used.' => '{attribute} 已经被使用。',
52 | 'Cannot be deleted because it has been used.' => '不能被删除,因为已经被使用。',
53 | 'Default account not found.' => '未找到默认账号',
54 | 'Accounting failed, the amount must be greater than 0.' => '记账失败,金额必须大于0。',
55 | 'The {attribute} not found.' => '{attribute} 未找到。',
56 | 'Upload file failed' => '上传文件失败',
57 | 'Category not found.' => '分类未找到',
58 | ];
59 |
--------------------------------------------------------------------------------
/core/messages/zh-CN/exception.php:
--------------------------------------------------------------------------------
1 | '参数异常',
7 | ErrorCodes::CANNOT_OPERATE_ERROR => '不能操作',
8 | ErrorCodes::THIRD_PARTY_SERVICE_ERROR => '第三方服务异常',
9 | ErrorCodes::FILE_ENCODING_ERROR => '文件编码必须为 UTF-8 格式',
10 | ];
11 |
--------------------------------------------------------------------------------
/core/models/AuthClient.php:
--------------------------------------------------------------------------------
1 | TimestampBehavior::class,
43 | 'value' => Yii::$app->formatter->asDatetime('now')
44 | ],
45 | ];
46 | }
47 |
48 | /**
49 | * {@inheritdoc}
50 | */
51 | public function rules()
52 | {
53 | return [
54 | [['user_id', 'type', 'client_id', 'status'], 'required'],
55 | [['user_id', 'type', 'status'], 'integer'],
56 | [['data'], 'string'],
57 | [['created_at', 'updated_at'], 'safe'],
58 | [['client_id', 'client_username'], 'string', 'max' => 255],
59 | [['user_id', 'type'], 'unique', 'targetAttribute' => ['user_id', 'type']],
60 | [['type', 'client_id'], 'unique', 'targetAttribute' => ['type', 'client_id']],
61 | ];
62 | }
63 |
64 | /**
65 | * {@inheritdoc}
66 | */
67 | public function attributeLabels()
68 | {
69 | return [
70 | 'id' => Yii::t('app', 'ID'),
71 | 'user_id' => Yii::t('app', 'User ID'),
72 | 'type' => Yii::t('app', 'Type'),
73 | 'client_id' => Yii::t('app', 'Client ID'),
74 | 'client_username' => Yii::t('app', 'Client Username'),
75 | 'data' => Yii::t('app', 'Data'),
76 | 'status' => Yii::t('app', 'Status'),
77 | 'created_at' => Yii::t('app', 'Created At'),
78 | 'updated_at' => Yii::t('app', 'Updated At'),
79 | ];
80 | }
81 |
82 | public function getUser()
83 | {
84 | return $this->hasOne(User::class, ['id' => 'user_id']);
85 | }
86 |
87 | /**
88 | * @return array
89 | */
90 | public function fields()
91 | {
92 | $fields = parent::fields();
93 | unset($fields['status'], $fields['data'], $fields['user_id'], $fields['client_id']);
94 |
95 | $fields['type'] = function (self $model) {
96 | return AuthClientType::getName($model->type);
97 | };
98 |
99 | $fields['created_at'] = function (self $model) {
100 | return DateHelper::datetimeToIso8601($model->created_at);
101 | };
102 |
103 | $fields['updated_at'] = function (self $model) {
104 | return DateHelper::datetimeToIso8601($model->updated_at);
105 | };
106 |
107 | return $fields;
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/core/models/Category.php:
--------------------------------------------------------------------------------
1 | TimestampBehavior::class,
49 | 'value' => Yii::$app->formatter->asDatetime('now')
50 | ],
51 | ];
52 | }
53 |
54 | /**
55 | * {@inheritdoc}
56 | */
57 | public function rules()
58 | {
59 | return [
60 | [['transaction_type', 'name', 'icon_name'], 'required'],
61 | [['user_id', 'status', 'default', 'sort'], 'integer'],
62 | ['transaction_type', 'in', 'range' => TransactionType::names()],
63 | [['name', 'icon_name'], 'string', 'max' => 120],
64 | ['color', 'in', 'range' => ColorType::items()],
65 | [
66 | 'name',
67 | 'unique',
68 | 'targetAttribute' => ['user_id', 'name'],
69 | 'message' => Yii::t('app', 'The {attribute} has been used.')
70 | ],
71 | ];
72 | }
73 |
74 | /**
75 | * {@inheritdoc}
76 | */
77 | public function attributeLabels()
78 | {
79 | return [
80 | 'id' => Yii::t('app', 'ID'),
81 | 'user_id' => Yii::t('app', 'User ID'),
82 | 'transaction_type' => Yii::t('app', 'Transaction Type'),
83 | 'name' => Yii::t('app', 'Name'),
84 | 'color' => Yii::t('app', 'Color'),
85 | 'icon_name' => Yii::t('app', 'Icon Name'),
86 | 'status' => Yii::t('app', 'Status'),
87 | 'default' => Yii::t('app', 'Default'),
88 | 'sort' => Yii::t('app', 'Sort'),
89 | 'created_at' => Yii::t('app', 'Created At'),
90 | 'updated_at' => Yii::t('app', 'Updated At'),
91 | ];
92 | }
93 |
94 | public function beforeValidate()
95 | {
96 | if (parent::beforeValidate()) {
97 | $this->user_id = Yii::$app->user->id;
98 | return true;
99 | }
100 | return false;
101 | }
102 |
103 |
104 | /**
105 | * @param bool $insert
106 | * @return bool
107 | * @throws InvalidArgumentException
108 | */
109 | public function beforeSave($insert)
110 | {
111 | if (parent::beforeSave($insert)) {
112 | if ($insert) {
113 | $ran = ColorType::items();
114 | $this->color = $this->color ?: $ran[mt_rand(0, count($ran) - 1)];
115 | }
116 | $this->transaction_type = TransactionType::toEnumValue($this->transaction_type);
117 | return true;
118 | } else {
119 | return false;
120 | }
121 | }
122 |
123 | /**
124 | * @return array
125 | */
126 | public function fields()
127 | {
128 | $fields = parent::fields();
129 | unset($fields['user_id']);
130 |
131 | $fields['transaction_type'] = function (self $model) {
132 | return TransactionType::getName($model->transaction_type);
133 | };
134 |
135 | $fields['transaction_type_text'] = function (self $model) {
136 | return data_get(TransactionType::texts(), $model->transaction_type);
137 | };
138 |
139 | $fields['created_at'] = function (self $model) {
140 | return DateHelper::datetimeToIso8601($model->created_at);
141 | };
142 |
143 | $fields['updated_at'] = function (self $model) {
144 | return DateHelper::datetimeToIso8601($model->updated_at);
145 | };
146 |
147 | return $fields;
148 | }
149 | }
150 |
--------------------------------------------------------------------------------
/core/models/CurrencyRate.php:
--------------------------------------------------------------------------------
1 | 3],
39 | [['currency_name'], 'string', 'max' => 60],
40 | ];
41 | }
42 |
43 | /**
44 | * {@inheritdoc}
45 | */
46 | public function attributeLabels()
47 | {
48 | return [
49 | 'id' => Yii::t('app', 'ID'),
50 | 'user_id' => Yii::t('app', 'User ID'),
51 | 'currency_code' => Yii::t('app', 'Currency Code'),
52 | 'currency_name' => Yii::t('app', 'Currency Name'),
53 | 'rate' => Yii::t('app', 'Rate'),
54 | 'status' => Yii::t('app', 'Status'),
55 | 'created_at' => Yii::t('app', 'Created At'),
56 | 'updated_at' => Yii::t('app', 'Updated At'),
57 | ];
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/core/models/Rule.php:
--------------------------------------------------------------------------------
1 | TimestampBehavior::class,
55 | 'value' => Yii::$app->formatter->asDatetime('now')
56 | ],
57 | ];
58 | }
59 |
60 | /**
61 | * {@inheritdoc}
62 | */
63 | public function rules()
64 | {
65 | return [
66 | [['name', 'if_keywords', 'then_transaction_type'], 'required'],
67 | [['user_id', 'then_category_id', 'then_from_account_id', 'then_to_account_id', 'sort'], 'integer'],
68 | ['status', 'in', 'range' => RuleStatus::names()],
69 | ['then_transaction_type', 'in', 'range' => TransactionType::names()],
70 | ['then_reimbursement_status', 'in', 'range' => ReimbursementStatus::names()],
71 | ['then_transaction_status', 'in', 'range' => TransactionStatus::names()],
72 | [['if_keywords', 'then_tags'], ArrayValidator::class],
73 | [['name'], 'string', 'max' => 255],
74 | ];
75 | }
76 |
77 | /**
78 | * {@inheritdoc}
79 | */
80 | public function attributeLabels()
81 | {
82 | return [
83 | 'id' => Yii::t('app', 'ID'),
84 | 'user_id' => Yii::t('app', 'User ID'),
85 | 'name' => Yii::t('app', 'Name'),
86 | 'if_keywords' => Yii::t('app', 'If Keywords'),
87 | 'then_transaction_type' => Yii::t('app', 'Then Transaction Type'),
88 | 'then_category_id' => Yii::t('app', 'Then Category ID'),
89 | 'then_from_account_id' => Yii::t('app', 'Then From Account ID'),
90 | 'then_to_account_id' => Yii::t('app', 'Then To Account ID'),
91 | 'then_transaction_status' => Yii::t('app', 'Then Transaction Status'),
92 | 'then_reimbursement_status' => Yii::t('app', 'Then Reimbursement Status'),
93 | 'then_tags' => Yii::t('app', 'Then Tags'),
94 | 'status' => Yii::t('app', 'Status'),
95 | 'sort' => Yii::t('app', 'Sort'),
96 | 'created_at' => Yii::t('app', 'Created At'),
97 | 'updated_at' => Yii::t('app', 'Updated At'),
98 | ];
99 | }
100 |
101 | /**
102 | * @param bool $insert
103 | * @return bool
104 | * @throws InvalidArgumentException
105 | */
106 | public function beforeSave($insert)
107 | {
108 | if (parent::beforeSave($insert)) {
109 | if ($insert) {
110 | $this->user_id = Yii::$app->user->id;
111 | }
112 | $this->then_reimbursement_status = is_null($this->then_reimbursement_status) ?
113 | ReimbursementStatus::NONE : ReimbursementStatus::toEnumValue($this->then_reimbursement_status);
114 | $this->then_transaction_status = is_null($this->then_transaction_status) ?
115 | TransactionStatus::DONE : TransactionStatus::toEnumValue($this->then_transaction_status);
116 |
117 | $this->status = is_null($this->status) ? RuleStatus::ACTIVE : RuleStatus::toEnumValue($this->status);
118 | $this->then_transaction_type = TransactionType::toEnumValue($this->then_transaction_type);
119 | $this->if_keywords = $this->if_keywords ? implode(',', $this->if_keywords) : null;
120 | $this->then_tags = $this->then_tags ? implode(',', $this->then_tags) : null;
121 | return true;
122 | } else {
123 | return false;
124 | }
125 | }
126 |
127 | public function getThenCategory()
128 | {
129 | return $this->hasOne(Category::class, ['id' => 'then_category_id']);
130 | }
131 |
132 | /**
133 | * @return array
134 | */
135 | public function fields()
136 | {
137 | $fields = parent::fields();
138 | unset($fields['user_id']);
139 |
140 | $fields['then_transaction_type'] = function (self $model) {
141 | return TransactionType::getName($model->then_transaction_type);
142 | };
143 |
144 | $fields['then_transaction_type_text'] = function (self $model) {
145 | return data_get(TransactionType::texts(), $model->then_transaction_type);
146 | };
147 |
148 | $fields['then_tags'] = function (self $model) {
149 | return $model->then_tags ? explode(',', $model->then_tags) : [];
150 | };
151 | $fields['thenCategory'] = function (self $model) {
152 | return $model->thenCategory;
153 | };
154 |
155 | $fields['if_keywords'] = function (self $model) {
156 | return explode(',', $model->if_keywords);
157 | };
158 |
159 | $fields['status'] = function (self $model) {
160 | return RuleStatus::getName($model->status);
161 | };
162 |
163 | $fields['then_reimbursement_status'] = function (self $model) {
164 | return ReimbursementStatus::getName($model->then_reimbursement_status);
165 | };
166 |
167 | $fields['then_transaction_status'] = function (self $model) {
168 | return TransactionStatus::getName($model->then_transaction_status);
169 | };
170 |
171 | $fields['created_at'] = function (self $model) {
172 | return DateHelper::datetimeToIso8601($model->created_at);
173 | };
174 |
175 | $fields['updated_at'] = function (self $model) {
176 | return DateHelper::datetimeToIso8601($model->updated_at);
177 | };
178 |
179 | return $fields;
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/core/models/Tag.php:
--------------------------------------------------------------------------------
1 | TimestampBehavior::class,
43 | 'value' => Yii::$app->formatter->asDatetime('now')
44 | ],
45 | ];
46 | }
47 |
48 | /**
49 | * {@inheritdoc}
50 | */
51 | public function rules()
52 | {
53 | return [
54 | [['name'], 'required'],
55 | [['user_id', 'count'], 'integer'],
56 | [['color'], 'string', 'max' => 7],
57 | [['name'], 'string', 'max' => 60],
58 | [
59 | 'name',
60 | 'unique',
61 | 'targetAttribute' => ['user_id', 'name'],
62 | 'message' => Yii::t('app', 'The {attribute} has been used.')
63 | ],
64 | ];
65 | }
66 |
67 | /**
68 | * {@inheritdoc}
69 | */
70 | public function attributeLabels()
71 | {
72 | return [
73 | 'id' => Yii::t('app', 'ID'),
74 | 'user_id' => Yii::t('app', 'User ID'),
75 | 'color' => Yii::t('app', 'Color'),
76 | 'name' => Yii::t('app', 'Name'),
77 | 'count' => Yii::t('app', 'Count'),
78 | 'created_at' => Yii::t('app', 'Created At'),
79 | 'updated_at' => Yii::t('app', 'Updated At'),
80 | ];
81 | }
82 |
83 | public function beforeValidate()
84 | {
85 | if (parent::beforeValidate()) {
86 | $this->user_id = Yii::$app->user->id;
87 | return true;
88 | }
89 | return false;
90 | }
91 |
92 | /**
93 | * @param bool $insert
94 | * @return bool
95 | */
96 | public function beforeSave($insert)
97 | {
98 | if (parent::beforeSave($insert)) {
99 | if ($insert) {
100 | $ran = ColorType::items();
101 | $this->color = $this->color ?: $ran[mt_rand(0, count($ran) - 1)];
102 | }
103 | return true;
104 | } else {
105 | return false;
106 | }
107 | }
108 |
109 | /**
110 | * @param bool $insert
111 | * @param array $changedAttributes
112 | * @throws \yii\db\Exception|\Throwable
113 | */
114 | public function afterSave($insert, $changedAttributes)
115 | {
116 | parent::afterSave($insert, $changedAttributes);
117 |
118 | if ($oldName = data_get($changedAttributes, 'name', '')) {
119 | TagService::updateTagName($oldName, $this->name);
120 | }
121 | }
122 |
123 | /**
124 | * @return bool
125 | * @throws CannotOperateException
126 | */
127 | public function beforeDelete()
128 | {
129 | if (TransactionService::countTransactionByTag($this->name, $this->user_id)) {
130 | throw new CannotOperateException(Yii::t('app', 'Cannot be deleted because it has been used.'));
131 | }
132 | return parent::beforeDelete();
133 | }
134 |
135 | /**
136 | * @return array
137 | */
138 | public function fields()
139 | {
140 | $fields = parent::fields();
141 | unset($fields['user_id']);
142 |
143 | $fields['created_at'] = function (self $model) {
144 | return DateHelper::datetimeToIso8601($model->created_at);
145 | };
146 |
147 | $fields['updated_at'] = function (self $model) {
148 | return DateHelper::datetimeToIso8601($model->updated_at);
149 | };
150 |
151 | return $fields;
152 | }
153 | }
154 |
--------------------------------------------------------------------------------
/core/models/User.php:
--------------------------------------------------------------------------------
1 | TimestampBehavior::class,
49 | 'value' => Yii::$app->formatter->asDatetime('now')
50 | ],
51 | ];
52 | }
53 |
54 | /**
55 | * @inheritdoc
56 | */
57 | public function rules()
58 | {
59 | return [
60 | ['status', 'default', 'value' => UserStatus::ACTIVE],
61 | ['status', 'in', 'range' => [UserStatus::ACTIVE, UserStatus::UNACTIVATED]],
62 | [['username'], 'string', 'max' => 60],
63 | ];
64 | }
65 |
66 | /**
67 | * @inheritdoc
68 | */
69 | public static function findIdentity($id)
70 | {
71 | return static::find()
72 | ->where(['id' => $id, 'status' => UserStatus::ACTIVE])
73 | ->limit(1)
74 | ->one();
75 | }
76 |
77 |
78 | /**
79 | * @param mixed $token
80 | * @param null $type
81 | * @return void|IdentityInterface
82 | */
83 | public static function findIdentityByAccessToken($token, $type = null)
84 | {
85 | $userId = (string)$token->getClaim('id');
86 | return self::findIdentity($userId);
87 | }
88 |
89 | /**
90 | * {@inheritdoc}
91 | */
92 | public function getId()
93 | {
94 | return $this->getPrimaryKey();
95 | }
96 |
97 | /**
98 | * {@inheritdoc}
99 | */
100 | public function getAuthKey()
101 | {
102 | return $this->auth_key;
103 | }
104 |
105 | /**
106 | * @inheritdoc
107 | */
108 | public function validateAuthKey($authKey)
109 | {
110 | return $this->getAuthKey() === $authKey;
111 | }
112 |
113 | /**
114 | * Validates password
115 | *
116 | * @param string $password password to validate
117 | * @return bool if password provided is valid for current user
118 | */
119 | public function validatePassword($password)
120 | {
121 | return Yii::$app->security->validatePassword($password, $this->password_hash);
122 | }
123 |
124 | /**
125 | * Generates password hash from password and sets it to the model
126 | *
127 | * @param string $password
128 | * @throws \yii\base\Exception
129 | */
130 | public function setPassword($password)
131 | {
132 | $this->password_hash = Yii::$app->security->generatePasswordHash($password);
133 | }
134 |
135 | /**
136 | * Generates "remember me" authentication key
137 | * @throws \yii\base\Exception
138 | */
139 | public function generateAuthKey()
140 | {
141 | $this->auth_key = Yii::$app->security->generateRandomString();
142 | }
143 |
144 | /**
145 | * Generates new password reset token
146 | * @throws \yii\base\Exception
147 | */
148 | public function generatePasswordResetToken()
149 | {
150 | $this->password_reset_token = Yii::$app->security->generateRandomString() . '_' . time();
151 | }
152 |
153 | /**
154 | * Removes password reset token
155 | */
156 | public function removePasswordResetToken()
157 | {
158 | $this->password_reset_token = null;
159 | }
160 |
161 | /**
162 | * Finds user by password reset token
163 | *
164 | * @param string $token password reset token
165 | * @return User|array|ActiveRecord|null
166 | */
167 | public static function findByPasswordResetToken($token)
168 | {
169 | if (!static::isPasswordResetTokenValid($token)) {
170 | return null;
171 | }
172 |
173 | return static::find()
174 | ->where(['password_reset_token' => $token, 'status' => [UserStatus::ACTIVE]])
175 | ->one();
176 | }
177 |
178 | /**
179 | * Finds out if password reset token is valid
180 | *
181 | * @param string $token password reset token
182 | * @return boolean
183 | */
184 | public static function isPasswordResetTokenValid($token)
185 | {
186 | if (empty($token)) {
187 | return false;
188 | }
189 |
190 | $timestamp = (int)substr($token, strrpos($token, '_') + 1);
191 | $expire = Yii::$app->params['user.passwordResetTokenExpire'];
192 | return $timestamp + $expire >= time();
193 | }
194 |
195 | /**
196 | * @return array
197 | */
198 | public function fields()
199 | {
200 | $fields = parent::fields();
201 | unset($fields['auth_key'], $fields['password_hash'], $fields['password_reset_token']);
202 |
203 | $fields['created_at'] = function (self $model) {
204 | return DateHelper::datetimeToIso8601($model->created_at);
205 | };
206 |
207 | $fields['avatar'] = function (self $model) {
208 | $avatar = md5(strtolower(trim($model->avatar)));
209 | return "https://www.gravatar.com/avatar/{$avatar}?s=48";
210 | };
211 |
212 | $fields['updated_at'] = function (self $model) {
213 | return DateHelper::datetimeToIso8601($model->updated_at);
214 | };
215 |
216 | return $fields;
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/core/requests/JoinRequest.php:
--------------------------------------------------------------------------------
1 | User::class],
25 | ['username', 'string', 'min' => 3, 'max' => 60],
26 |
27 | ['email', 'string', 'min' => 2, 'max' => 120],
28 | ['email', 'unique', 'targetClass' => User::class],
29 | ['email', 'email'],
30 |
31 | ['password', 'required'],
32 | ['password', 'string', 'min' => 6],
33 |
34 | ['base_currency_code', 'in', 'range' => CurrencyCode::getKeys()],
35 | ];
36 | }
37 |
38 | /**
39 | * @inheritdoc
40 | */
41 | public function attributeLabels()
42 | {
43 | return [
44 | 'username' => t('app', 'Username'),
45 | 'password' => t('app', 'Password'),
46 | 'email' => t('app', 'Email'),
47 | 'base_currency_code' => t('app', 'Base Currency Code'),
48 | ];
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/core/requests/LoginRequest.php:
--------------------------------------------------------------------------------
1 | 'trim'],
20 | [['username', 'password'], 'required'],
21 |
22 | ['password', 'validatePassword'],
23 | ];
24 | }
25 |
26 | public function validatePassword($attribute, $params)
27 | {
28 | if (!$this->hasErrors()) {
29 | $user = UserService::getUserByUsernameOrEmail($this->username);
30 | if (!$user || !$user->validatePassword($this->password)) {
31 | $this->addError($attribute, t('app', 'Incorrect username or password.'));
32 | }
33 | Yii::$app->user->setIdentity($user);
34 | }
35 | }
36 |
37 | /**
38 | * @inheritdoc
39 | */
40 | public function attributeLabels()
41 | {
42 | return [
43 | 'username' => t('app', 'Username'),
44 | 'password' => t('app', 'Password'),
45 | 'email' => t('app', 'Email'),
46 | ];
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/core/requests/RecurrenceUpdateStatusRequest.php:
--------------------------------------------------------------------------------
1 | RecurrenceStatus::names()],
19 | ];
20 | }
21 |
22 | /**
23 | * @inheritdoc
24 | */
25 | public function attributeLabels()
26 | {
27 | return [
28 | 'status' => t('app', 'Status'),
29 | ];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/core/requests/RuleUpdateStatusRequest.php:
--------------------------------------------------------------------------------
1 | RuleStatus::names()],
19 | ];
20 | }
21 |
22 | /**
23 | * @inheritdoc
24 | */
25 | public function attributeLabels()
26 | {
27 | return [
28 | 'status' => t('app', 'Status'),
29 | ];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/core/requests/TransactionCreateByDescRequest.php:
--------------------------------------------------------------------------------
1 | 255],
17 | ];
18 | }
19 |
20 | /**
21 | * @inheritdoc
22 | */
23 | public function attributeLabels()
24 | {
25 | return [
26 | 'description' => t('app', 'Description'),
27 | ];
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/core/requests/TransactionUploadRequest.php:
--------------------------------------------------------------------------------
1 | false,
20 | 'extensions' => 'csv',
21 | 'checkExtensionByMimeType' => false,
22 | 'maxSize' => 1 * 1024 * 1024
23 | ],
24 | ];
25 | }
26 |
27 | /**
28 | * @inheritdoc
29 | */
30 | public function attributeLabels()
31 | {
32 | return [
33 | 'file' => t('app', 'file'),
34 | ];
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/core/services/AccountService.php:
--------------------------------------------------------------------------------
1 | Yii::$app->user->id];
23 | Record::deleteAll($baseConditions + ['account_id' => $account->id]);
24 |
25 | $transactionIds = Transaction::find()
26 | ->where([
27 | 'and',
28 | $baseConditions,
29 | ['or', ['from_account_id' => $account->id], ['to_account_id' => $account->id]]
30 | ])
31 | ->column();
32 | Transaction::deleteAll($baseConditions + ['id' => $transactionIds]);
33 | Recurrence::deleteAll($baseConditions + ['transaction_id' => $transactionIds]);
34 |
35 | Rule::deleteAll([
36 | 'and',
37 | $baseConditions,
38 | ['or', ['then_from_account_id' => $account->id], ['then_to_account_id' => $account->id]]
39 | ]);
40 | }
41 |
42 | /**
43 | * @param Account $account
44 | * @return Account
45 | * @throws InternalException
46 | */
47 | public function createUpdate(Account $account): Account
48 | {
49 | try {
50 | $account->user_id = Yii::$app->user->id;
51 | if (!$account->save()) {
52 | throw new \yii\db\Exception(Setup::errorMessage($account->firstErrors));
53 | }
54 | } catch (Exception $e) {
55 | Yii::error(
56 | ['request_id' => Yii::$app->requestId->id, $account->attributes, $account->errors, (string)$e],
57 | __FUNCTION__
58 | );
59 | throw new InternalException($e->getMessage());
60 | }
61 | return Account::findOne($account->id);
62 | }
63 |
64 |
65 | /**
66 | * @param int $id
67 | * @return Account|ActiveRecord|null
68 | */
69 | public static function findCurrentOne(int $id)
70 | {
71 | return Account::find()->where(['id' => $id])
72 | ->andWhere(['user_id' => Yii::$app->user->id])
73 | ->one();
74 | }
75 |
76 | public static function getDefaultAccount(int $userId = 0)
77 | {
78 | $userId = $userId ?: Yii::$app->user->id;
79 | return Account::find()
80 | ->where(['user_id' => $userId, 'default' => Account::DEFAULT])
81 | ->orderBy(['id' => SORT_ASC])
82 | ->asArray()
83 | ->one();
84 | }
85 |
86 | /**
87 | * @param int $accountId
88 | * @return bool
89 | * @throws \yii\db\Exception
90 | */
91 | public static function updateAccountBalance(int $accountId): bool
92 | {
93 | if (!$model = self::findCurrentOne($accountId)) {
94 | throw new \yii\db\Exception('not found account');
95 | }
96 | $model->load($model->toArray(), '');
97 | $model->currency_balance = Setup::toYuan(self::getCalculateCurrencyBalanceCent($accountId));
98 | if (!$model->save()) {
99 | Yii::error(
100 | ['request_id' => Yii::$app->requestId->id, $model->attributes, $model->errors],
101 | __FUNCTION__
102 | );
103 | throw new \yii\db\Exception('update account failure ' . Setup::errorMessage($model->firstErrors));
104 | }
105 | return true;
106 | }
107 |
108 |
109 | /**
110 | * @param int $accountId
111 | * @return int
112 | */
113 | public static function getCalculateCurrencyBalanceCent(int $accountId): int
114 | {
115 | $in = Record::find()->where([
116 | 'account_id' => $accountId,
117 | 'direction' => DirectionType::INCOME,
118 | ])->sum('currency_amount_cent');
119 |
120 | $out = Record::find()->where([
121 | 'account_id' => $accountId,
122 | 'direction' => DirectionType::EXPENSE,
123 | ])->sum('currency_amount_cent');
124 |
125 | return ($in - $out) ?: 0;
126 | }
127 |
128 | /**
129 | * @return array
130 | */
131 | public static function getCurrentMap()
132 | {
133 | $accounts = Account::find()->where(['user_id' => Yii::$app->user->id])->asArray()->all();
134 | return ArrayHelper::map($accounts, 'id', 'name');
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/core/services/CategoryService.php:
--------------------------------------------------------------------------------
1 | user->id;
18 | return Category::find()
19 | ->where(['user_id' => $userId, 'default' => Category::DEFAULT])
20 | ->orderBy(['id' => SORT_ASC])
21 | ->asArray()
22 | ->one();
23 | }
24 |
25 | public static function getAdjustCategoryId(int $userId = 0)
26 | {
27 | $userId = $userId ?: Yii::$app->user->id;
28 | return Category::find()
29 | ->where(['user_id' => $userId, 'transaction_type' => TransactionType::ADJUST])
30 | ->orderBy(['id' => SORT_ASC])
31 | ->scalar();
32 | }
33 |
34 | /**
35 | * @param int $id
36 | * @return Account|ActiveRecord|null
37 | * @throws NotFoundHttpException
38 | */
39 | public static function findCurrentOne(int $id)
40 | {
41 | if (!$model = Category::find()->where(['id' => $id, 'user_id' => \Yii::$app->user->id])->one()) {
42 | throw new NotFoundHttpException('No data found');
43 | }
44 | return $model;
45 | }
46 |
47 | /**
48 | * @return array
49 | */
50 | public static function getCurrentMap()
51 | {
52 | $categories = Category::find()->where(['user_id' => Yii::$app->user->id])->asArray()->all();
53 | return ArrayHelper::map($categories, 'id', 'name');
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/core/services/RecurrenceService.php:
--------------------------------------------------------------------------------
1 | findCurrentOne($id);
37 | $model->load($model->toArray(), '');
38 | if (RecurrenceStatus::ACTIVE == RecurrenceStatus::toEnumValue($status)) {
39 | $model->started_at = strtotime($model->started_at) > time() ? $model->started_at : 'now';
40 | }
41 | $model->started_at = Yii::$app->formatter->asDate($model->started_at);
42 | $model->status = $status;
43 | if (!$model->save()) {
44 | throw new Exception(Setup::errorMessage($model->firstErrors));
45 | }
46 | return $model;
47 | }
48 |
49 |
50 | /**
51 | * @param int $id
52 | * @return Recurrence|object
53 | * @throws NotFoundHttpException
54 | */
55 | public function findCurrentOne(int $id): Recurrence
56 | {
57 | if (!$model = Recurrence::find()->where(['id' => $id, 'user_id' => \Yii::$app->user->id])->one()) {
58 | throw new NotFoundHttpException('No data found');
59 | }
60 | return $model;
61 | }
62 |
63 | /**
64 | * @param Recurrence $recurrence
65 | * @return string|null
66 | * @throws InvalidConfigException
67 | * @throws \Exception
68 | */
69 | public static function getExecutionDate(Recurrence $recurrence)
70 | {
71 | $formatter = Yii::$app->formatter;
72 | switch ($recurrence->frequency) {
73 | case RecurrenceFrequency::DAY:
74 | $date = strtotime("+1 day", strtotime($recurrence->started_at));
75 | break;
76 | case RecurrenceFrequency::WEEK:
77 | $currentWeekDay = $formatter->asDatetime('now', 'php:N') - 1;
78 | $weekDay = $recurrence->schedule;
79 | $addDay = $currentWeekDay > $weekDay ? 7 - $currentWeekDay + $weekDay : $weekDay - $currentWeekDay;
80 | $date = strtotime("+{$addDay} day", strtotime($recurrence->started_at));
81 | break;
82 |
83 | case RecurrenceFrequency::MONTH:
84 | $currDay = $formatter->asDatetime('now', 'php:d');
85 | $d = $recurrence->schedule;
86 | $date = Yii::$app->formatter->asDatetime($currDay > $d ? strtotime('+1 month') : time(), 'php:Y-m');
87 | $d = sprintf("%02d", $d);
88 | return "{$date}-{$d}";
89 | case RecurrenceFrequency::YEAR:
90 | $m = current(explode('-', $recurrence->schedule));
91 | $currMonth = $formatter->asDatetime('now', 'php:m');
92 | $y = Yii::$app->formatter->asDatetime($currMonth > $m ? strtotime('+1 year') : time(), 'php:Y');
93 | return "{$y}-{$recurrence->schedule}";
94 | case RecurrenceFrequency::WORKING_DAY:
95 | if (($currentWeekDay = $formatter->asDatetime('now', 'php:N') - 1) > 5) {
96 | return null;
97 | }
98 | $date = strtotime("+1 day", strtotime($recurrence->started_at));
99 | break;
100 | case RecurrenceFrequency::LEGAL_WORKING_DAY:
101 | return HolidayHelper::getNextWorkday();
102 | default:
103 | return null;
104 | }
105 | return $formatter->asDatetime($date, 'php:Y-m-d');
106 | }
107 |
108 | /**
109 | * @param int $transactionId
110 | * @param int $userId
111 | * @return bool|int|string|null
112 | */
113 | public static function countByTransactionId(int $transactionId, int $userId)
114 | {
115 | return Recurrence::find()
116 | ->where(['user_id' => $userId, 'transaction_id' => $transactionId])
117 | ->count();
118 | }
119 |
120 | /**
121 | * @throws InvalidConfigException|ThirdPartyServiceErrorException
122 | */
123 | public static function updateAllExecutionDate()
124 | {
125 | $items = Recurrence::find()
126 | ->where(['status' => RecurrenceStatus::ACTIVE])
127 | ->andWhere(['!=', 'frequency', RecurrenceFrequency::LEGAL_WORKING_DAY])
128 | ->all();
129 | /** @var Recurrence $item */
130 | foreach ($items as $item) {
131 | $date = self::getExecutionDate($item);
132 | Recurrence::updateAll(
133 | ['execution_date' => $date, 'updated_at' => Yii::$app->formatter->asDatetime('now')],
134 | ['id' => $item->id]
135 | );
136 | }
137 | self::updateAllLegalWorkingDay();
138 | }
139 |
140 | /**
141 | * @throws InvalidConfigException
142 | * @throws ThirdPartyServiceErrorException
143 | */
144 | private static function updateAllLegalWorkingDay()
145 | {
146 | $items = Recurrence::find()
147 | ->where(['frequency' => RecurrenceFrequency::LEGAL_WORKING_DAY, 'status' => RecurrenceStatus::ACTIVE])
148 | ->asArray()
149 | ->all();
150 | $nextWorkday = HolidayHelper::getNextWorkday();
151 | foreach ($items as $key => $item) {
152 | Recurrence::updateAll(
153 | ['execution_date' => $nextWorkday, 'updated_at' => Yii::$app->formatter->asDatetime('now')],
154 | ['id' => $item['id']]
155 | );
156 | }
157 | }
158 | }
159 |
--------------------------------------------------------------------------------
/core/services/RuleService.php:
--------------------------------------------------------------------------------
1 | findCurrentOne($id);
23 | $rule = new Rule();
24 | $values = $model->toArray();
25 | $rule->load($values, '');
26 | $rule->name = $rule->name . ' Copy';
27 | if (!$rule->save(false)) {
28 | throw new Exception(Setup::errorMessage($rule->firstErrors));
29 | }
30 | return Rule::findOne($rule->id);
31 | }
32 |
33 | /**
34 | * @param int $id
35 | * @param string $status
36 | * @return Rule
37 | * @throws Exception
38 | * @throws NotFoundHttpException
39 | */
40 | public function updateStatus(int $id, string $status)
41 | {
42 | $model = $this->findCurrentOne($id);
43 | $model->load($model->toArray(), '');
44 | $model->status = $status;
45 | if (!$model->save(false)) {
46 | throw new Exception(Setup::errorMessage($model->firstErrors));
47 | }
48 | return $model;
49 | }
50 |
51 |
52 | /**
53 | * @param string $desc
54 | * @return Rule[]
55 | */
56 | public function getRulesByDesc(string $desc)
57 | {
58 | $models = Rule::find()
59 | ->where(['user_id' => \Yii::$app->user->id, 'status' => RuleStatus::ACTIVE])
60 | ->orderBy(['sort' => SORT_ASC, 'id' => SORT_DESC])
61 | ->all();
62 | $rules = [];
63 | /** @var Rule $model */
64 | foreach ($models as $model) {
65 | if (ArrayHelper::strPosArr($desc, explode(',', $model->if_keywords)) !== false) {
66 | array_push($rules, $model);
67 | }
68 | }
69 | return $rules;
70 | }
71 |
72 | /**
73 | * @param int $id
74 | * @return Rule|object
75 | * @throws NotFoundHttpException
76 | */
77 | public function findCurrentOne(int $id): Rule
78 | {
79 | if (!$model = Rule::find()->where(['id' => $id, 'user_id' => \Yii::$app->user->id])->one()) {
80 | throw new NotFoundHttpException('No data found');
81 | }
82 | return $model;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/core/services/TagService.php:
--------------------------------------------------------------------------------
1 | user->id;
17 | return Tag::find()->select('name')->where(['user_id' => $userId])->column();
18 | }
19 |
20 | /**
21 | * @param array $data
22 | * @return Tag
23 | * @throws Exception
24 | */
25 | public function create(array $data)
26 | {
27 | $model = new Tag();
28 | $model->load($data, '');
29 | $model->user_id = Yii::$app->user->id;
30 | if (!$model->save(false)) {
31 | throw new Exception(Setup::errorMessage($model->firstErrors));
32 | }
33 | return $model;
34 | }
35 |
36 | /**
37 | * @param array $tags
38 | * @param int $userId
39 | * @throws \yii\base\InvalidConfigException
40 | */
41 | public static function updateCounters(array $tags, int $userId = 0)
42 | {
43 | $userId = $userId ?: Yii::$app->user->id;
44 | foreach ($tags as $tag) {
45 | $count = TransactionService::countTransactionByTag($tag, $userId);
46 | Tag::updateAll(
47 | ['count' => $count, 'updated_at' => Yii::$app->formatter->asDatetime('now')],
48 | ['user_id' => $userId, 'name' => $tag]
49 | );
50 | }
51 | }
52 |
53 | /**
54 | * @param string $oldName
55 | * @param string $newName
56 | * @param int $userId
57 | * @throws \yii\base\InvalidConfigException
58 | */
59 | public static function updateTagName(string $oldName, string $newName, int $userId = 0)
60 | {
61 | $userId = $userId ?: Yii::$app->user->id;
62 | $items = Transaction::find()
63 | ->where(['user_id' => $userId])
64 | ->andWhere(new Expression('FIND_IN_SET(:tag, tags)'))->addParams([':tag' => $oldName])
65 | ->asArray()
66 | ->all();
67 | /** @var Transaction $item */
68 | foreach ($items as $item) {
69 | $tags = str_replace($oldName, $newName, $item['tags']);
70 | Transaction::updateAll(
71 | ['tags' => $tags, 'updated_at' => Yii::$app->formatter->asDatetime('now')],
72 | ['id' => $item['id']]
73 | );
74 | }
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/core/services/UploadService.php:
--------------------------------------------------------------------------------
1 | saveFile($uploadedFile, $filename)) {
25 | throw new \Exception(Yii::t('app', 'Upload file failed'));
26 | }
27 | $this->checkEncoding($filename);
28 | return $this->getFullFilename($filename, params('uploadWebPath'));
29 | } catch (\Exception $e) {
30 | Log::error('upload record error', [$uploadedFile, (string)$e]);
31 | throw new \Exception($e->getMessage(), $e->getCode());
32 | }
33 | }
34 |
35 | /**
36 | * @param $uploadedFile UploadedFile
37 | * @param string $filename
38 | * @return bool
39 | * @throws \yii\base\Exception
40 | */
41 | protected function saveFile(UploadedFile $uploadedFile, string $filename)
42 | {
43 | $filename = $this->getFullFilename($filename);
44 | $this->deleteLocalFile($filename);
45 | FileHelper::createDirectory(dirname($filename));
46 | return $uploadedFile->saveAs($filename);
47 | }
48 |
49 |
50 | /**
51 | * @param $filename
52 | * @param $path
53 | * @return bool|string
54 | */
55 | public function getFullFilename($filename, string $path = '')
56 | {
57 | $path = $path ?: params('uploadSavePath');
58 | $filename = Yii::getAlias(rtrim($path, '/') . '/' . $filename);
59 | return $filename;
60 | }
61 |
62 | /**
63 | * @param string $filename
64 | */
65 | public function deleteLocalFile(string $filename)
66 | {
67 | $fileAbsoluteName = $this->getFullFilename($filename);
68 | @unlink($fileAbsoluteName);
69 | }
70 |
71 | /**
72 | * @param string $filename
73 | * @param string $encoding
74 | * @throws FileException
75 | */
76 | public function checkEncoding(string $filename, $encoding = 'UTF-8')
77 | {
78 | $fileAbsoluteName = $this->getFullFilename($filename);
79 | if (!mb_check_encoding(file_get_contents($fileAbsoluteName), $encoding)) {
80 | @unlink($fileAbsoluteName);
81 | throw new FileException(Yii::t('app/error', ErrorCodes::FILE_ENCODING_ERROR));
82 | }
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/core/traits/FixDataTrait.php:
--------------------------------------------------------------------------------
1 | db->beginTransaction() : null;
31 | $count = $query->count();
32 | try {
33 | for ($i = 0; $i <= (int)ceil($count / $this->onceCount); $i++) {
34 | $items = $query
35 | ->orderBy(['id' => SORT_ASC])
36 | ->limit($this->onceCount)
37 | ->offset($i * $this->onceCount)
38 | ->all();
39 | // item is array
40 | foreach ($items as $item) {
41 | if (!call_user_func($existCallback, $item)) {
42 | // todo batch insert
43 | $ids[] = call_user_func($callback, $item);
44 | }
45 | }
46 | }
47 | $userTransaction ? $transaction->commit() : null;
48 | sleep($this->sleepSecond);
49 | return $ids ?? [];
50 | } catch (\Exception $e) {
51 | $userTransaction ? $transaction->rollBack() : null;
52 | \Yii::error('修复数据失败', ['query' => $query, (string)$e]);
53 | throw $e;
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/core/traits/SendRequestTrait.php:
--------------------------------------------------------------------------------
1 | $data,
27 | 'timeout' => 10000, // Response timeout
28 | 'connect_timeout' => 10000, // Connection timeout
29 | ];
30 | $client = new \GuzzleHttp\Client(['verify' => false]);
31 | $response = $client->request($type, $apiUrl, array_merge($baseOptions, $options));
32 | Log::info('Curl 请求服务开始', ['url' => $apiUrl, $data, (array)$response]);
33 | } catch (\Exception $e) {
34 | Log::error('Curl 请求服务异常', ['url' => $apiUrl, 'exception' => (string)$e, $data]);
35 | throw new ErrorException('Curl 请求异常:' . $e->getMessage(), 500001);
36 | }
37 | if ($response->getStatusCode() == 200) {
38 | // 记录 curl 耗时
39 | $endMillisecond = round(microtime(true) * 1000);
40 | $context = [
41 | 'curlUrl' => $apiUrl,
42 | 'CurlSpendingMillisecond' => $endMillisecond - $beginMillisecond
43 | ];
44 | Log::info('curl time consuming', [$context, $data,]);
45 | return (string)$response->getBody();
46 | } else {
47 | Log::error('Curl 请求服务成功,但是操作失败', ['url' => $apiUrl, 'data' => $data]);
48 | throw new ErrorException('Curl 请求服务成功,但是操作失败:');
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/core/traits/ServiceTrait.php:
--------------------------------------------------------------------------------
1 |
5 | * createTime : 2019/5/12 4:58 PM
6 | * description:
7 | */
8 |
9 | namespace app\core\traits;
10 |
11 | use app\core\services\AccountService;
12 | use app\core\services\AnalysisService;
13 | use app\core\services\CategoryService;
14 | use app\core\services\RecurrenceService;
15 | use app\core\services\RuleService;
16 | use app\core\services\TagService;
17 | use app\core\services\TelegramService;
18 | use app\core\services\TransactionService;
19 | use app\core\services\UploadService;
20 | use app\core\services\UserService;
21 | use Yii;
22 | use yii\base\InvalidConfigException;
23 |
24 | /**
25 | * Trait ServiceTrait
26 | * @property UserService $userService
27 | * @property AccountService $accountService
28 | * @property TransactionService $transactionService
29 | * @property CategoryService $categoryService
30 | * @property RuleService $ruleService
31 | * @property TelegramService $telegramService
32 | * @property AnalysisService $analysisService
33 | * @property TagService $tagService
34 | * @property RecurrenceService $recurrenceService
35 | * @property UploadService $uploadService
36 | */
37 | trait ServiceTrait
38 | {
39 | /**
40 | * @return UserService|object
41 | */
42 | public function getUserService()
43 | {
44 | try {
45 | return Yii::createObject(UserService::class);
46 | } catch (InvalidConfigException $e) {
47 | return new UserService();
48 | }
49 | }
50 |
51 |
52 | /**
53 | * @return AccountService|object
54 | */
55 | public function getAccountService()
56 | {
57 | try {
58 | return Yii::createObject(AccountService::class);
59 | } catch (InvalidConfigException $e) {
60 | return new AccountService();
61 | }
62 | }
63 |
64 | /**
65 | * @return TransactionService|object
66 | */
67 | public function getTransactionService()
68 | {
69 | try {
70 | return Yii::createObject(TransactionService::class);
71 | } catch (InvalidConfigException $e) {
72 | return new TransactionService();
73 | }
74 | }
75 |
76 | /**
77 | * @return CategoryService|object
78 | */
79 | public function getCategoryService()
80 | {
81 | try {
82 | return Yii::createObject(CategoryService::class);
83 | } catch (InvalidConfigException $e) {
84 | return new CategoryService();
85 | }
86 | }
87 |
88 | /**
89 | * @return RuleService|object
90 | */
91 | public function getRuleService()
92 | {
93 | try {
94 | return Yii::createObject(RuleService::class);
95 | } catch (InvalidConfigException $e) {
96 | return new RuleService();
97 | }
98 | }
99 |
100 |
101 | /**
102 | * @return TelegramService|object
103 | */
104 | public function getTelegramService()
105 | {
106 | try {
107 | return Yii::createObject(TelegramService::class);
108 | } catch (InvalidConfigException $e) {
109 | return new TelegramService();
110 | }
111 | }
112 |
113 | /**
114 | * @return AnalysisService|object
115 | */
116 | public function getAnalysisService()
117 | {
118 | try {
119 | return Yii::createObject(AnalysisService::class);
120 | } catch (InvalidConfigException $e) {
121 | return new AnalysisService();
122 | }
123 | }
124 |
125 | /**
126 | * @return TagService|object
127 | */
128 | public function getTagService()
129 | {
130 | try {
131 | return Yii::createObject(TagService::class);
132 | } catch (InvalidConfigException $e) {
133 | return new TagService();
134 | }
135 | }
136 |
137 | /**
138 | * @return RecurrenceService|object
139 | */
140 | public function getRecurrenceService()
141 | {
142 | try {
143 | return Yii::createObject(RecurrenceService::class);
144 | } catch (InvalidConfigException $e) {
145 | return new RecurrenceService();
146 | }
147 | }
148 |
149 | /**
150 | * @return UploadService|object
151 | */
152 | public function getUploadService()
153 | {
154 | try {
155 | return Yii::createObject(UploadService::class);
156 | } catch (InvalidConfigException $e) {
157 | return new UploadService();
158 | }
159 | }
160 | }
161 |
--------------------------------------------------------------------------------
/core/types/AccountStatus.php:
--------------------------------------------------------------------------------
1 | 'general_account',
31 | self::CASH_ACCOUNT => 'cash_account',
32 | self::DEBIT_CARD => 'debit_card',
33 | self::CREDIT_CARD => 'credit_card',
34 | self::SAVING_ACCOUNT => 'saving_account',
35 | self::INVESTMENT_ACCOUNT => 'investment_account',
36 | ];
37 | }
38 |
39 | public static function texts(): array
40 | {
41 | return [
42 | self::GENERAL_ACCOUNT => Yii::t('app', 'General Account'),
43 | self::CASH_ACCOUNT => Yii::t('app', 'Cash Account'),
44 | self::DEBIT_CARD => Yii::t('app', 'Debit Card'),
45 | self::CREDIT_CARD => Yii::t('app', 'Credit Card'),
46 | self::SAVING_ACCOUNT => Yii::t('app', 'Saving Account'),
47 | self::INVESTMENT_ACCOUNT => Yii::t('app', 'Investment Account'),
48 | ];
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/core/types/AnalysisDateType.php:
--------------------------------------------------------------------------------
1 | Yii::t('app', 'Today'),
35 | self::YESTERDAY => Yii::t('app', 'Yesterday'),
36 | self::CURRENT_MONTH => Yii::t('app', 'Current month'),
37 | self::LAST_MONTH => Yii::t('app', 'Last month'),
38 | self::GRAND_TOTAL => Yii::t('app', 'Grand total')
39 | ];
40 | }
41 |
42 |
43 | /**
44 | * @param string $dateStr
45 | * @return array
46 | * @throws InvalidConfigException
47 | * @throws \Exception
48 | */
49 | public static function getEveryDayByMonth(string $dateStr)
50 | {
51 | $formatter = Yii::$app->formatter;
52 | $items = [];
53 | [$y, $m, $lastDay] = explode('-', date("Y-m-t", strtotime($dateStr)));
54 | for ($i = 1; $i <= $lastDay; $i++) {
55 | $time = date("{$y}-{$m}-" . sprintf("%02d", $i));
56 | $date = [DateHelper::beginTimestamp($time), DateHelper::endTimestamp($time)];
57 | $items[] = array_map(function ($i) use ($formatter) {
58 | return $formatter->asDatetime($i);
59 | }, $date);
60 | }
61 | return $items;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/core/types/AnalysisGroupDateType.php:
--------------------------------------------------------------------------------
1 | '%Y-%m-%d',
22 | self::MONTH => '%Y-%m',
23 | self::YEAR => '%Y',
24 | ];
25 | try {
26 | return $items[$key];
27 | } catch (\ErrorException $e) {
28 | throw new InvalidArgumentException(sprintf('Invalid: %s const value %s', __CLASS__, $key));
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/core/types/AuthClientStatus.php:
--------------------------------------------------------------------------------
1 | 'telegram',
13 | ];
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/core/types/BaseStatus.php:
--------------------------------------------------------------------------------
1 | 'active',
17 | self::UNACTIVATED => 'unactivated',
18 | ];
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/core/types/BaseType.php:
--------------------------------------------------------------------------------
1 | t('app', 'Yuan Renminbi'),
15 | 'USD' => t('app', 'US Dollar'),
16 | 'EUR' => t('app', 'Euro'),
17 | 'GBP' => t('app', 'Pound Sterling'),
18 | 'JPY' => t('app', 'Japanese Yen'),
19 | 'AUD' => t('app', 'Australian Dollar'),
20 | 'CAD' => t('app', 'Canadian Dollar'),
21 | 'CHF' => t('app', 'Swiss Franc'),
22 | 'HKD' => t('app', 'Hong Kong Dollar'),
23 | 'SEK' => t('app', 'Swedish Krona'),
24 | ];
25 | }
26 |
27 | /**
28 | * @return array
29 | */
30 | public static function getKeys(): array
31 | {
32 | return array_keys(self::names());
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/core/types/DirectionType.php:
--------------------------------------------------------------------------------
1 | 'expense',
14 | self::INCOME => 'income',
15 | ];
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/core/types/RecordSource.php:
--------------------------------------------------------------------------------
1 | 'web',
16 | self::TELEGRAM => 'telegram',
17 | self::CRONTAB => 'crontab',
18 | self::IMPORT => 'import',
19 | ];
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/core/types/RecurrenceFrequency.php:
--------------------------------------------------------------------------------
1 | 'day',
19 | self::WEEK => 'week',
20 | self::MONTH => 'month',
21 | self::YEAR => 'year',
22 | self::WORKING_DAY => 'working_day',
23 | self::LEGAL_WORKING_DAY => 'legal_working_day',
24 | ];
25 | }
26 |
27 | public static function texts()
28 | {
29 | return [
30 | self::DAY => '每天',
31 | self::WEEK => '每周',
32 | self::MONTH => '每月',
33 | self::YEAR => '每年',
34 | self::WORKING_DAY => '工作日(周一至周五)',
35 | self::LEGAL_WORKING_DAY => '法定工作日',
36 | ];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/core/types/RecurrenceStatus.php:
--------------------------------------------------------------------------------
1 | 'none',
20 | self::TODO => 'todo',
21 | self::DONE => 'done',
22 | ];
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/core/types/RuleStatus.php:
--------------------------------------------------------------------------------
1 | 'must',
17 | self::NEED => 'need',
18 | self::WANT => 'want',
19 | ];
20 | }
21 |
22 | public static function texts(): array
23 | {
24 | return [
25 | self::MUST => Yii::t('app', 'Must'),
26 | self::NEED => Yii::t('app', 'Need'),
27 | self::WANT => Yii::t('app', 'Want'),
28 | ];
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/core/types/TransactionStatus.php:
--------------------------------------------------------------------------------
1 | 'todo',
17 | self::DONE => 'done',
18 | ];
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/core/types/TransactionType.php:
--------------------------------------------------------------------------------
1 | 'expense',
18 | self::INCOME => 'income',
19 | self::TRANSFER => 'transfer',
20 | self::ADJUST => 'adjust',
21 | ];
22 | }
23 |
24 | public static function texts(): array
25 | {
26 | return [
27 | self::EXPENSE => Yii::t('app', 'Expense'),
28 | self::INCOME => Yii::t('app', 'Income'),
29 | self::TRANSFER => Yii::t('app', 'Transfer'),
30 | self::ADJUST => Yii::t('app', 'Adjust Balance'),
31 | ];
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/core/types/UserStatus.php:
--------------------------------------------------------------------------------
1 |
2 | # The ServerName directive sets the request scheme, hostname and port that
3 | # the server uses to identify itself. This is used when creating
4 | # redirection URLs. In the context of virtual hosts, the ServerName
5 | # specifies what hostname must appear in the request's Host: header to
6 | # match this virtual host. For the default virtual host (this file) this
7 | # value is not decisive as it is used as a last resort host regardless.
8 | # However, you must set it for any further virtual host explicitly.
9 | #ServerName www.example.com
10 |
11 | ServerAdmin webmaster@localhost
12 | DocumentRoot /srv/web
13 |
14 | # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
15 | # error, crit, alert, emerg.
16 | # It is also possible to configure the loglevel for particular
17 | # modules, e.g.
18 | #LogLevel info ssl:warn
19 |
20 | ErrorLog ${APACHE_LOG_DIR}/error.log
21 | CustomLog ${APACHE_LOG_DIR}/access.log combined
22 |
23 |
24 |
25 |
26 | Options Indexes FollowSymLinks
27 | AllowOverride All
28 | Require all granted
29 |
30 |
31 |
32 | # For most configuration files from conf-available/, which are
33 | # enabled or disabled at a global level, it is possible to
34 | # include a line for only one particular virtual host. For example the
35 | # following line enables the CGI configuration for this host only
36 | # after it has been globally disabled with "a2disconf".
37 | #Include conf-available/serve-cgi-bin.conf
38 |
39 |
40 | # vim: syntax=apache ts=4 sw=4 sts=4 sr noet
41 |
--------------------------------------------------------------------------------
/image-files/etc/apache2/sites-available/default-ssl.config:
--------------------------------------------------------------------------------
1 |
2 |
3 | ServerAdmin webmaster@localhost
4 |
5 | DocumentRoot /srv/web
6 |
7 | # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
8 | # error, crit, alert, emerg.
9 | # It is also possible to configure the loglevel for particular
10 | # modules, e.g.
11 | #LogLevel info ssl:warn
12 |
13 | ErrorLog ${APACHE_LOG_DIR}/error.log
14 | CustomLog ${APACHE_LOG_DIR}/access.log combined
15 |
16 | # For most configuration files from conf-available/, which are
17 | # enabled or disabled at a global level, it is possible to
18 | # include a line for only one particular virtual host. For example the
19 | # following line enables the CGI configuration for this host only
20 | # after it has been globally disabled with "a2disconf".
21 | #Include conf-available/serve-cgi-bin.conf
22 |
23 | # SSL Engine Switch:
24 | # Enable/Disable SSL for this virtual host.
25 | SSLEngine on
26 |
27 | # A self-signed (snakeoil) certificate can be created by installing
28 | # the ssl-cert package. See
29 | # /usr/share/doc/apache2/README.Debian.gz for more info.
30 | # If both key and certificate are stored in the same file, only the
31 | # SSLCertificateFile directive is needed.
32 | SSLCertificateFile /etc/ssl/certs/ssl-cert-snakeoil.pem
33 | SSLCertificateKeyFile /etc/ssl/private/ssl-cert-snakeoil.key
34 |
35 | # Server Certificate Chain:
36 | # Point SSLCertificateChainFile at a file containing the
37 | # concatenation of PEM encoded CA certificates which form the
38 | # certificate chain for the server certificate. Alternatively
39 | # the referenced file can be the same as SSLCertificateFile
40 | # when the CA certificates are directly appended to the server
41 | # certificate for convinience.
42 | #SSLCertificateChainFile /etc/apache2/ssl.crt/server-ca.crt
43 |
44 | # Certificate Authority (CA):
45 | # Set the CA certificate verification path where to find CA
46 | # certificates for client authentication or alternatively one
47 | # huge file containing all of them (file must be PEM encoded)
48 | # Note: Inside SSLCACertificatePath you need hash symlinks
49 | # to point to the certificate files. Use the provided
50 | # Makefile to update the hash symlinks after changes.
51 | #SSLCACertificatePath /etc/ssl/certs/
52 | #SSLCACertificateFile /etc/apache2/ssl.crt/ca-bundle.crt
53 |
54 | # Certificate Revocation Lists (CRL):
55 | # Set the CA revocation path where to find CA CRLs for client
56 | # authentication or alternatively one huge file containing all
57 | # of them (file must be PEM encoded)
58 | # Note: Inside SSLCARevocationPath you need hash symlinks
59 | # to point to the certificate files. Use the provided
60 | # Makefile to update the hash symlinks after changes.
61 | #SSLCARevocationPath /etc/apache2/ssl.crl/
62 | #SSLCARevocationFile /etc/apache2/ssl.crl/ca-bundle.crl
63 |
64 | # Client Authentication (Type):
65 | # Client certificate verification type and depth. Types are
66 | # none, optional, require and optional_no_ca. Depth is a
67 | # number which specifies how deeply to verify the certificate
68 | # issuer chain before deciding the certificate is not valid.
69 | #SSLVerifyClient require
70 | #SSLVerifyDepth 10
71 |
72 | # SSL Engine Options:
73 | # Set various options for the SSL engine.
74 | # o FakeBasicAuth:
75 | # Translate the client X.509 into a Basic Authorisation. This means that
76 | # the standard Auth/DBMAuth methods can be used for access control. The
77 | # user name is the `one line' version of the client's X.509 certificate.
78 | # Note that no password is obtained from the user. Every entry in the user
79 | # file needs this password: `xxj31ZMTZzkVA'.
80 | # o ExportCertData:
81 | # This exports two additional environment variables: SSL_CLIENT_CERT and
82 | # SSL_SERVER_CERT. These contain the PEM-encoded certificates of the
83 | # server (always existing) and the client (only existing when client
84 | # authentication is used). This can be used to import the certificates
85 | # into CGI scripts.
86 | # o StdEnvVars:
87 | # This exports the standard SSL/TLS related `SSL_*' environment variables.
88 | # Per default this exportation is switched off for performance reasons,
89 | # because the extraction step is an expensive operation and is usually
90 | # useless for serving static content. So one usually enables the
91 | # exportation for CGI and SSI requests only.
92 | # o OptRenegotiate:
93 | # This enables optimized SSL connection renegotiation handling when SSL
94 | # directives are used in per-directory context.
95 | #SSLOptions +FakeBasicAuth +ExportCertData +StrictRequire
96 |
97 | SSLOptions +StdEnvVars
98 |
99 |
100 | SSLOptions +StdEnvVars
101 |
102 |
103 | # SSL Protocol Adjustments:
104 | # The safe and default but still SSL/TLS standard compliant shutdown
105 | # approach is that mod_ssl sends the close notify alert but doesn't wait for
106 | # the close notify alert from client. When you need a different shutdown
107 | # approach you can use one of the following variables:
108 | # o ssl-unclean-shutdown:
109 | # This forces an unclean shutdown when the connection is closed, i.e. no
110 | # SSL close notify alert is send or allowed to received. This violates
111 | # the SSL/TLS standard but is needed for some brain-dead browsers. Use
112 | # this when you receive I/O errors because of the standard approach where
113 | # mod_ssl sends the close notify alert.
114 | # o ssl-accurate-shutdown:
115 | # This forces an accurate shutdown when the connection is closed, i.e. a
116 | # SSL close notify alert is send and mod_ssl waits for the close notify
117 | # alert of the client. This is 100% SSL/TLS standard compliant, but in
118 | # practice often causes hanging connections with brain-dead browsers. Use
119 | # this only for browsers where you know that their SSL implementation
120 | # works correctly.
121 | # Notice: Most problems of broken clients are also related to the HTTP
122 | # keep-alive facility, so you usually additionally want to disable
123 | # keep-alive for those clients, too. Use variable "nokeepalive" for this.
124 | # Similarly, one has to force some clients to use HTTP/1.0 to workaround
125 | # their broken HTTP/1.1 implementation. Use variables "downgrade-1.0" and
126 | # "force-response-1.0" for this.
127 | # BrowserMatch "MSIE [2-6]" \
128 | # nokeepalive ssl-unclean-shutdown \
129 | # downgrade-1.0 force-response-1.0
130 |
131 |
132 |
133 |
134 | # vim: syntax=apache ts=4 sw=4 sts=4 sr noet
135 |
--------------------------------------------------------------------------------
/image-files/root/.bashrc:
--------------------------------------------------------------------------------
1 | cat <<'MSG'
2 | _ _ __ _
3 | (_|_)/ _| | |
4 | _ _ _ _| |_ _ __ __ _ _ __ ___ _____ _____ _ __| | __
5 | | | | | | | _| '__/ _` | '_ ` _ \ / _ \ \ /\ / / _ \| '__| |/ /
6 | | |_| | | | | | | | (_| | | | | | | __/\ V V / (_) | | | <
7 | \__, |_|_|_| |_| \__,_|_| |_| |_|\___| \_/\_/ \___/|_| |_|\_\
8 | __/ |
9 | |___/
10 |
11 | MSG
12 |
13 | echo "PHP version: ${PHP_VERSION}"
14 |
--------------------------------------------------------------------------------
/image-files/usr/local/bin/docker-entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | printf "Checking database connection...\n\n"
4 | mysql_ready() {
5 | mysqladmin ping --host=mysql --user=root --password=$MYSQL_ROOT_PASSWORD >/dev/null 2>&1
6 | }
7 |
8 | while !(mysql_ready); do
9 | sleep 3
10 | echo "Waiting for database connection ..."
11 | done
12 |
13 | printf "Upgrading database...\n\n"
14 | ./yii migrate/up --interactive=0
15 |
16 | touch /srv/.migrated
17 |
18 | exec "$@"
19 |
--------------------------------------------------------------------------------
/image-files/usr/local/etc/php/conf.d/base.ini:
--------------------------------------------------------------------------------
1 | ; Required timezone
2 | date.timezone = Australia/Melbourne
3 |
4 | ; General settings
5 | memory_limit = 256M
6 | max_execution_time = 300
7 | sys_temp_dir = /tmp
8 | upload_max_filesize = 512M
9 | upload_tmp_dir = /tmp
10 | post_max_size = 512M
11 |
12 | ; Security, Debug & Logs
13 | expose_php = off
14 | cgi.fix_pathinfo = 0
15 | log_errors = on
16 | error_reporting = E_ALL
17 | html_errors = on
18 | xdebug.default_enable = off
19 |
20 | ; Opcache
21 | opcache.memory_consumption = 128
22 | opcache.interned_strings_buffer = 8
23 | opcache.max_accelerated_files = 4000
24 | ;opcache.validate_timestamps=off
25 | opcache.fast_shutdown = 0
26 | opcache.enable_cli = 1
27 |
28 | ; PHP language options
29 | short_open_tag = 0
30 |
--------------------------------------------------------------------------------
/image-files/usr/local/etc/php/conf.d/xdebug.ini:
--------------------------------------------------------------------------------
1 | ;zend_extension=xdebug.so
2 | xdebug.remote_enable = 0
3 | xdebug.remote_autostart = 0
4 | xdebug.remote_connect_back = 1
5 | xdebug.remote_port = 9000
6 | xdebug.idekey = PHPStorm
7 | xdebug.extended_info = 1
8 |
--------------------------------------------------------------------------------
/mail/layouts/html.php:
--------------------------------------------------------------------------------
1 |
9 | beginPage() ?>
10 |
11 |
12 |
13 |
14 | = Html::encode($this->title) ?>
15 | head() ?>
16 |
17 |
18 | beginBody() ?>
19 | = $content ?>
20 | endBody() ?>
21 |
22 |
23 | endPage() ?>
24 |
--------------------------------------------------------------------------------
/migrations/m200717_082932_create_user_table.php:
--------------------------------------------------------------------------------
1 | createTable('{{%user}}', [
16 | 'id' => $this->primaryKey(),
17 | 'username' => $this->string(60)->notNull()->unique(),
18 | 'avatar' => $this->string()->defaultValue(''),
19 | 'email' => $this->string(120)->notNull()->unique(),
20 | 'auth_key' => $this->string()->notNull(),
21 | 'password_hash' => $this->string()->notNull(),
22 | 'password_reset_token' => $this->string()->defaultValue(''),
23 | 'status' => $this->tinyInteger()->defaultValue(0),
24 | 'base_currency_code' => $this->string(3)->notNull(),
25 | 'created_at' => $this->timestamp()->defaultValue(null),
26 | 'updated_at' => $this->timestamp()->defaultValue(null),
27 | ]);
28 | }
29 |
30 | /**
31 | * {@inheritdoc}
32 | */
33 | public function safeDown()
34 | {
35 | $this->dropTable('{{%user}}');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/migrations/m200730_062450_create_account_table.php:
--------------------------------------------------------------------------------
1 | createTable('{{%account}}', [
16 | 'id' => $this->primaryKey(),
17 | 'user_id' => $this->integer()->notNull(),
18 | 'name' => $this->string(120)->notNull(),
19 | 'type' => $this->tinyInteger()->notNull(),
20 | 'color' => $this->string(7)->notNull(),
21 | 'balance_cent' => $this->bigInteger()->defaultValue(0),
22 | 'currency_code' => $this->string(3)->notNull(),
23 | 'status' => $this->tinyInteger()->defaultValue(1),
24 | 'exclude_from_stats' => $this->tinyInteger()->defaultValue(0),
25 | 'credit_card_limit' => $this->integer(),
26 | 'credit_card_repayment_day' => $this->tinyInteger(),
27 | 'credit_card_billing_day' => $this->tinyInteger(),
28 | 'created_at' => $this->timestamp()->defaultValue(null),
29 | 'updated_at' => $this->timestamp()->defaultValue(null),
30 | ]);
31 |
32 | $this->createIndex('account_user_id', '{{%account}}', 'user_id');
33 | }
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | public function safeDown()
39 | {
40 | $this->dropTable('{{%account}}');
41 | }
42 |
43 | }
44 |
--------------------------------------------------------------------------------
/migrations/m200730_065801_create_category_table.php:
--------------------------------------------------------------------------------
1 | createTable('{{%category}}', [
16 | 'id' => $this->primaryKey(),
17 | 'user_id' => $this->integer()->notNull(),
18 | 'direction' => $this->tinyInteger()->notNull(),
19 | 'name' => $this->string(120)->notNull(),
20 | 'color' => $this->string(7)->notNull(),
21 | 'icon_name' => $this->string(120)->notNull(),
22 | 'status' => $this->tinyInteger()->defaultValue(1),
23 | 'created_at' => $this->timestamp()->defaultValue(null),
24 | 'updated_at' => $this->timestamp()->defaultValue(null),
25 | ]);
26 |
27 | $this->createIndex('category_user_id', '{{%category}}', 'user_id');
28 | }
29 |
30 | /**
31 | * {@inheritdoc}
32 | */
33 | public function safeDown()
34 | {
35 | $this->dropTable('{{%category}}');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/migrations/m200730_082622_create_currency_rate_table.php:
--------------------------------------------------------------------------------
1 | createTable('{{%currency_rate}}', [
16 | 'id' => $this->primaryKey(),
17 | 'user_id' => $this->integer()->notNull(),
18 | 'currency_code' => $this->string(3)->notNull(),
19 | 'currency_name' => $this->string(60)->notNull(),
20 | 'rate' => $this->bigInteger(),
21 | 'status' => $this->tinyInteger()->defaultValue(1),
22 | 'created_at' => $this->timestamp()->defaultValue(null),
23 | 'updated_at' => $this->timestamp()->defaultValue(null),
24 | ]);
25 |
26 | $this->createIndex('currency_rate_user_id', '{{%currency_rate}}', 'user_id');
27 | }
28 |
29 | /**
30 | * {@inheritdoc}
31 | */
32 | public function safeDown()
33 | {
34 | $this->dropTable('{{%currency_rate}}');
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/migrations/m200730_085233_create_tag_table.php:
--------------------------------------------------------------------------------
1 | createTable('{{%tag}}', [
16 | 'id' => $this->primaryKey(),
17 | 'user_id' => $this->integer()->notNull(),
18 | 'color' => $this->string(7)->notNull(),
19 | 'name' => $this->string(60)->notNull(),
20 | 'count' => $this->integer()->defaultValue(0),
21 | 'created_at' => $this->timestamp()->defaultValue(null),
22 | 'updated_at' => $this->timestamp()->defaultValue(null),
23 | ]);
24 |
25 | $this->createIndex('tag_user_id', '{{%tag}}', 'user_id');
26 | }
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | public function safeDown()
32 | {
33 | $this->dropTable('{{%tag}}');
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/migrations/m200812_090059_add_default_column.php:
--------------------------------------------------------------------------------
1 | addColumn(
16 | '{{%category}}',
17 | 'default',
18 | $this->tinyInteger()->defaultValue(0)->after('status')
19 | );
20 |
21 | $this->addColumn(
22 | '{{%account}}',
23 | 'default',
24 | $this->tinyInteger()->defaultValue(0)->after('credit_card_billing_day')
25 | );
26 | }
27 |
28 | /**
29 | * {@inheritdoc}
30 | */
31 | public function safeDown()
32 | {
33 | echo "m200812_090059_add_default_column cannot be reverted.\n";
34 | $this->dropColumn('{{%category}}', 'default');
35 | $this->dropColumn('{{%account}}', 'default');
36 | return true;
37 | }
38 |
39 | /*
40 | // Use up()/down() to run migration code without a transaction.
41 | public function up()
42 | {
43 |
44 | }
45 |
46 | public function down()
47 | {
48 | echo "m200812_090059_add_default_column cannot be reverted.\n";
49 |
50 | return false;
51 | }
52 | */
53 | }
54 |
--------------------------------------------------------------------------------
/migrations/m200814_032606_create_rule_table.php:
--------------------------------------------------------------------------------
1 | createTable('{{%rule}}', [
16 | 'id' => $this->primaryKey(),
17 | 'user_id' => $this->integer()->notNull(),
18 | 'name' => $this->string()->notNull(),
19 | 'if_keywords' => $this->string()->notNull()->comment('Multiple choice use,'),
20 | 'if_direction' => $this->tinyInteger()->defaultValue(0)->comment('0:any'),
21 | 'then_direction' => $this->tinyInteger(),
22 | 'then_category_id' => $this->integer(),
23 | 'then_account_id' => $this->integer(),
24 | 'then_transaction_status' => $this->tinyInteger(),
25 | 'then_reimbursement_status' => $this->tinyInteger(),
26 | 'then_tags' => $this->string()->comment('Multiple choice use,'),
27 | 'status' => $this->tinyInteger()->defaultValue(1),
28 | 'created_at' => $this->timestamp()->defaultValue(null),
29 | 'updated_at' => $this->timestamp()->defaultValue(null),
30 | ]);
31 |
32 | $this->createIndex('rule_user_id', '{{%rule}}', 'user_id');
33 | }
34 |
35 | /**
36 | * {@inheritdoc}
37 | */
38 | public function safeDown()
39 | {
40 | $this->dropTable('{{%rule}}');
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/migrations/m200818_021303_update_rules_table.php:
--------------------------------------------------------------------------------
1 | renameColumn('{{%rule}}', 'then_account_id', 'then_from_account_id');
16 | $this->addColumn('{{%rule}}', 'then_to_account_id', $this->integer()->after('then_from_account_id'));
17 | $this->renameColumn('{{%rule}}', 'if_direction', 'then_transaction_type');
18 | $this->dropColumn('{{%rule}}', 'then_direction');
19 | }
20 |
21 | /**
22 | * {@inheritdoc}
23 | */
24 | public function safeDown()
25 | {
26 | echo "m200818_021303_update_rules_table cannot be reverted.\n";
27 | $this->renameColumn('{{%rule}}', 'then_from_account_id', 'then_account_id');
28 | $this->dropColumn('{{%rule}}', 'then_to_account_id');
29 | $this->addColumn('{{%rule}}', 'then_direction', $this->tinyInteger()->after('then_transaction_type'));
30 | $this->renameColumn('{{%rule}}', 'then_transaction_type', 'if_direction');
31 | return true;
32 | }
33 |
34 | /*
35 | // Use up()/down() to run migration code without a transaction.
36 | public function up()
37 | {
38 |
39 | }
40 |
41 | public function down()
42 | {
43 | echo "m200818_021303_update_rules_table cannot be reverted.\n";
44 |
45 | return false;
46 | }
47 | */
48 | }
49 |
--------------------------------------------------------------------------------
/migrations/m200818_130329_create_transaction_table.php:
--------------------------------------------------------------------------------
1 | createTable('{{%transaction}}', [
16 | 'id' => $this->primaryKey(),
17 | 'user_id' => $this->integer()->notNull(),
18 | 'from_account_id' => $this->integer(),
19 | 'to_account_id' => $this->integer(),
20 | 'type' => $this->tinyInteger()->notNull(),
21 | 'category_id' => $this->integer()->notNull(),
22 | 'amount_cent' => $this->integer()->notNull(), // base currency
23 | 'currency_amount_cent' => $this->integer()->notNull(),
24 | 'currency_code' => $this->string(3)->notNull(),
25 | 'tags' => $this->string()->comment('Multiple choice use,'),
26 | 'description' => $this->string(),
27 | 'remark' => $this->string(),
28 | 'image' => $this->string(),
29 | 'status' => $this->tinyInteger()->defaultValue(1),
30 | 'reimbursement_status' => $this->tinyInteger(),
31 | 'rating' => $this->tinyInteger(),
32 | 'date' => $this->timestamp()->notNull(),
33 | 'created_at' => $this->timestamp()->defaultValue(null),
34 | 'updated_at' => $this->timestamp()->defaultValue(null),
35 | ]);
36 |
37 | $this->createIndex('transaction_user_id', '{{%transaction}}', 'user_id');
38 |
39 | $this->execute("ALTER TABLE {{%transaction}} ADD FULLTEXT INDEX `full_text` (`description`, `tags`, `remark`)");
40 | }
41 |
42 | /**
43 | * {@inheritdoc}
44 | */
45 | public function safeDown()
46 | {
47 | $this->dropTable('{{%transaction}}');
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/migrations/m200818_131101_create_record_table.php:
--------------------------------------------------------------------------------
1 | createTable('{{%record}}', [
16 | 'id' => $this->primaryKey(),
17 | 'user_id' => $this->integer()->notNull(),
18 | 'account_id' => $this->integer()->notNull(),
19 | 'category_id' => $this->integer()->notNull(),
20 | 'amount_cent' => $this->integer()->notNull(), // base currency
21 | 'currency_amount_cent' => $this->integer()->notNull(),
22 | 'currency_code' => $this->string(3)->notNull(),
23 | 'transaction_id' => $this->integer(),
24 | 'direction' => $this->tinyInteger()->notNull(),
25 | 'date' => $this->timestamp()->notNull(),
26 | 'created_at' => $this->timestamp()->defaultValue(null),
27 | 'updated_at' => $this->timestamp()->defaultValue(null),
28 | ]);
29 |
30 | $this->createIndex('record_user_id_transaction_id', '{{%record}}', ['user_id', 'transaction_id']);
31 | $this->createIndex('record_user_id_account_id', '{{%record}}', ['user_id', 'account_id']);
32 | }
33 |
34 | /**
35 | * {@inheritdoc}
36 | */
37 | public function safeDown()
38 | {
39 | $this->dropTable('{{%record}}');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/migrations/m200821_031033_update_category_table.php:
--------------------------------------------------------------------------------
1 | renameColumn('{{%category}}', 'direction', 'transaction_type');
16 | }
17 |
18 | /**
19 | * {@inheritdoc}
20 | */
21 | public function safeDown()
22 | {
23 | echo "m200821_031033_update_category_table cannot be reverted.\n";
24 | $this->renameColumn('{{%category}}', 'transaction_type', 'direction');
25 |
26 | return true;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/migrations/m200821_031118_update_account_table.php:
--------------------------------------------------------------------------------
1 | addColumn('{{%account}}', 'currency_balance_cent', $this->bigInteger()->after('balance_cent'));
16 | }
17 |
18 | /**
19 | * {@inheritdoc}
20 | */
21 | public function safeDown()
22 | {
23 | echo "m200821_031118_update_account_table cannot be reverted.\n";
24 | $this->dropColumn('{{%account}}', 'currency_balance_cent');
25 |
26 | return true;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/migrations/m200823_094104_create_auth_client_table.php:
--------------------------------------------------------------------------------
1 | createTable('{{%auth_client}}', [
16 | 'id' => $this->primaryKey(),
17 | 'user_id' => $this->integer()->notNull(),
18 | 'type' => $this->tinyInteger()->notNull(),
19 | 'client_id' => $this->string()->notNull(),
20 | 'client_username' => $this->string(),
21 | 'data' => $this->text(),
22 | 'status' => $this->tinyInteger()->notNull(),
23 | 'created_at' => $this->timestamp()->defaultValue(null),
24 | 'updated_at' => $this->timestamp()->defaultValue(null),
25 | ]);
26 |
27 | $this->createIndex('login_user_id_type', '{{%auth_client}}', ['user_id', 'type'], true);
28 | $this->createIndex('login_type_client_id', '{{%auth_client}}', ['type', 'client_id'], true);
29 | }
30 |
31 | /**
32 | * {@inheritdoc}
33 | */
34 | public function safeDown()
35 | {
36 | $this->dropTable('{{%auth_client}}');
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/migrations/m200828_095405_update_record_table.php:
--------------------------------------------------------------------------------
1 | alterColumn('{{record}}', 'date', $this->timestamp()->defaultValue(null));
16 | $this->alterColumn('{{transaction}}', 'date', $this->timestamp()->defaultValue(null));
17 |
18 | $this->addColumn('{{record}}', 'transaction_type', $this->tinyInteger()->after('account_id'));
19 | $this->addColumn('{{record}}', 'source', $this->tinyInteger()->after('date'));
20 | }
21 |
22 | /**
23 | * {@inheritdoc}
24 | */
25 | public function safeDown()
26 | {
27 | echo "m200828_095405_update_record_table cannot be reverted.\n";
28 | $this->dropColumn('{{record}}', 'transaction_type');
29 | $this->dropColumn('{{record}}', 'source');
30 |
31 | return true;
32 | }
33 |
34 | /*
35 | // Use up()/down() to run migration code without a transaction.
36 | public function up()
37 | {
38 |
39 | }
40 |
41 | public function down()
42 | {
43 | echo "m200828_095405_update_record_table cannot be reverted.\n";
44 |
45 | return false;
46 | }
47 | */
48 | }
49 |
--------------------------------------------------------------------------------
/migrations/m200830_030728_create_sort_column.php:
--------------------------------------------------------------------------------
1 | addColumn('{{rule}}', 'sort', $this->tinyInteger()->defaultValue(99)->after('status'));
16 | $this->addColumn('{{account}}', 'sort', $this->tinyInteger()->defaultValue(99)->after('default'));
17 | $this->addColumn('{{category}}', 'sort', $this->tinyInteger()->defaultValue(99)->after('default'));
18 | }
19 |
20 | /**
21 | * {@inheritdoc}
22 | */
23 | public function safeDown()
24 | {
25 | echo "m200830_030728_create_sort_column cannot be reverted.\n";
26 | $this->dropColumn('{{rule}}', 'sort');
27 | $this->dropColumn('{{account}}', 'sort');
28 | $this->dropColumn('{{category}}', 'sort');
29 |
30 | return true;
31 | }
32 |
33 | /*
34 | // Use up()/down() to run migration code without a transaction.
35 | public function up()
36 | {
37 |
38 | }
39 |
40 | public function down()
41 | {
42 | echo "m200830_030728_create_sort_column cannot be reverted.\n";
43 |
44 | return false;
45 | }
46 | */
47 | }
48 |
--------------------------------------------------------------------------------
/migrations/m200909_030624_create_recurrence_table.php:
--------------------------------------------------------------------------------
1 | createTable('{{%recurrence}}', [
16 | 'id' => $this->primaryKey(),
17 | 'user_id' => $this->integer()->notNull(),
18 | 'name' => $this->string()->notNull(),
19 | 'frequency' => $this->tinyInteger()->notNull(),
20 | 'interval' => $this->tinyInteger()->defaultValue(1),
21 | 'schedule' => $this->string(),
22 | 'transaction_id' => $this->integer()->notNull(),
23 | 'started_at' => $this->timestamp()->defaultValue(null),
24 | 'execution_date' => $this->timestamp()->defaultValue(null),
25 | 'status' => $this->tinyInteger()->defaultValue(1),
26 | 'created_at' => $this->timestamp()->defaultValue(null),
27 | 'updated_at' => $this->timestamp()->defaultValue(null),
28 | ]);
29 |
30 | $this->createIndex('record_user_id_transaction_id', '{{%recurrence}}', ['user_id', 'transaction_id']);
31 | }
32 |
33 | /**
34 | * {@inheritdoc}
35 | */
36 | public function safeDown()
37 | {
38 | $this->dropTable('{{%recurrence}}');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/migrations/m200917_064807_create_exclude_from_stats_column.php:
--------------------------------------------------------------------------------
1 | addColumn('{{%record}}', 'exclude_from_stats', $this->tinyInteger()->defaultValue(0)->after('source'));
16 | }
17 |
18 | /**
19 | * {@inheritdoc}
20 | */
21 | public function safeDown()
22 | {
23 | echo "m200917_064807_create_exclude_from_stats_column cannot be reverted.\n";
24 | $this->dropColumn('{{%record}}', 'exclude_from_stats');
25 | return true;
26 | }
27 |
28 | /*
29 | // Use up()/down() to run migration code without a transaction.
30 | public function up()
31 | {
32 |
33 | }
34 |
35 | public function down()
36 | {
37 | echo "m200917_064807_create_exclude_from_stats_column cannot be reverted.\n";
38 |
39 | return false;
40 | }
41 | */
42 | }
43 |
--------------------------------------------------------------------------------
/modules/v1/Module.php:
--------------------------------------------------------------------------------
1 | SORT_ASC, 'id' => SORT_DESC];
25 | public $partialMatchAttributes = ['name'];
26 | public $stringToIntAttributes = ['type' => AccountType::class, 'status' => AccountStatus::class];
27 |
28 | public function actions()
29 | {
30 | $actions = parent::actions();
31 | // 注销系统自带的实现方法
32 | unset($actions['update'], $actions['create']);
33 | return $actions;
34 | }
35 |
36 | /**
37 | * @return Account
38 | * @throws Exception
39 | */
40 | public function actionCreate()
41 | {
42 | $params = Yii::$app->request->bodyParams;
43 | $model = new Account();
44 | $model->user_id = 0;
45 | if (data_get($params, 'type') == AccountType::CREDIT_CARD) {
46 | $model->setScenario(AccountType::CREDIT_CARD);
47 | }
48 | /** @var Account $model */
49 | $model = $this->validate($model, $params);
50 |
51 | return $this->accountService->createUpdate($model);
52 | }
53 |
54 | /**
55 | * @param int $id
56 | * @return Account
57 | * @throws NotFoundHttpException
58 | * @throws Exception
59 | */
60 | public function actionUpdate(int $id)
61 | {
62 | $params = Yii::$app->request->bodyParams;
63 | if (!$model = AccountService::findCurrentOne($id)) {
64 | throw new NotFoundHttpException();
65 | }
66 |
67 | if (data_get($params, 'type') == AccountType::CREDIT_CARD) {
68 | $model->setScenario(AccountType::CREDIT_CARD);
69 | }
70 | /** @var Account $model */
71 | $model = $this->validate($model, $params);
72 |
73 | return $this->accountService->createUpdate($model);
74 | }
75 |
76 |
77 | /**
78 | * @return array
79 | * @throws Exception
80 | */
81 | public function actionTypes()
82 | {
83 | $items = [];
84 | $texts = AccountType::texts();
85 | foreach (AccountType::names() as $key => $name) {
86 | $items[] = ['type' => $name, 'name' => data_get($texts, $key)];
87 | }
88 | return $items;
89 | }
90 |
91 | /**
92 | * @return array
93 | */
94 | public function actionOverview()
95 | {
96 | $balanceCentSum = Account::find()
97 | ->where(['user_id' => Yii::$app->user->id, 'exclude_from_stats' => false])
98 | ->sum('balance_cent');
99 | $items['net_asset'] = $balanceCentSum ? Setup::toYuan($balanceCentSum) : 0;
100 |
101 | $balanceCentSum = Account::find()
102 | ->where(['user_id' => Yii::$app->user->id, 'exclude_from_stats' => false])
103 | ->andWhere(['>', 'balance_cent', 0])
104 | ->sum('balance_cent');
105 | $items['total_assets'] = $balanceCentSum ? Setup::toYuan($balanceCentSum) : 0;
106 |
107 | $balanceCentSum = Account::find()
108 | ->where(['user_id' => Yii::$app->user->id, 'exclude_from_stats' => false])
109 | ->andWhere(['<', 'balance_cent', 0])
110 | ->sum('balance_cent');
111 | $items['liabilities'] = $balanceCentSum ? Setup::toYuan($balanceCentSum) : 0;
112 |
113 | $items['count'] = Account::find()
114 | ->where(['user_id' => Yii::$app->user->id, 'exclude_from_stats' => false])
115 | ->count('id');
116 |
117 | return $items;
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/modules/v1/controllers/ActiveController.php:
--------------------------------------------------------------------------------
1 | SORT_DESC];
29 | public $partialMatchAttributes = [];
30 | public $stringToIntAttributes = [];
31 | public $relations = [];
32 |
33 | /**
34 | * 不参与校验的 actions
35 | * @var array
36 | */
37 | public $noAuthActions = [];
38 |
39 | // 序列化输出
40 | public $serializer = [
41 | 'class' => 'yii\rest\Serializer',
42 | 'collectionEnvelope' => 'items',
43 | ];
44 |
45 | public function behaviors()
46 | {
47 | $behaviors = parent::behaviors();
48 |
49 | // 跨区请求 必须先删掉 authenticator
50 | $behaviors['authenticator'];
51 | unset($behaviors['authenticator']);
52 |
53 | $behaviors['corsFilter'] = [
54 | 'class' => Cors::class,
55 | 'cors' => [
56 | 'Origin' => ['*'],
57 | 'Access-Control-Request-Method' => ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
58 | 'Access-Control-Request-Headers' => ['*'],
59 | 'Access-Control-Max-Age' => 86400,
60 | ]
61 | ];
62 | $behaviors['authenticator'] = [
63 | 'class' => JwtHttpBearerAuth::class,
64 | 'optional' => array_merge($this->noAuthActions, ['options']),
65 | ];
66 |
67 | return $behaviors;
68 | }
69 |
70 | public function actions()
71 | {
72 | $actions = parent::actions();
73 | $actions['index']['prepareDataProvider'] = [$this, 'prepareDataProvider'];
74 | $actions['create']['class'] = CreateAction::class;
75 | return $actions;
76 | }
77 |
78 | /**
79 | * @return ActiveDataProvider
80 | * @throws InvalidArgumentException
81 | * @throws InternalException
82 | * @throws InvalidConfigException
83 | * @throws \Exception
84 | */
85 | public function prepareDataProvider()
86 | {
87 | /** @var ActiveRecord $modelClass */
88 | $modelClass = $this->modelClass;
89 | $searchModel = new SearchModel([
90 | 'defaultOrder' => $this->defaultOrder,
91 | 'model' => $modelClass,
92 | 'relations' => $this->relations,
93 | 'scenario' => 'default',
94 | 'partialMatchAttributes' => $this->partialMatchAttributes,
95 | 'pageSize' => $this->getPageSize()
96 | ]);
97 |
98 | $params = $this->formatParams(Yii::$app->request->queryParams);
99 | foreach ($this->stringToIntAttributes as $attribute => $className) {
100 | if ($type = data_get($params, $attribute)) {
101 | $params[$attribute] = SearchHelper::stringToInt($type, $className);
102 | }
103 | }
104 | unset($params['sort']);
105 |
106 | $dataProvider = $searchModel->search(['SearchModel' => $params]);
107 | $dataProvider->query->andWhere([$modelClass::tableName() . '.user_id' => Yii::$app->user->id]);
108 | return $dataProvider;
109 | }
110 |
111 |
112 | protected function formatParams(array $params)
113 | {
114 | return $params;
115 | }
116 |
117 | /**
118 | * @return int
119 | */
120 | protected function getPageSize()
121 | {
122 | if ($pageSize = (int)request('pageSize')) {
123 | if ($pageSize < self::MAX_PAGE_SIZE) {
124 | return $pageSize;
125 | }
126 | return self::MAX_PAGE_SIZE;
127 | }
128 | return self::DEFAULT_PAGE_SIZE;
129 | }
130 |
131 |
132 | /**
133 | * @param Model $model
134 | * @param array $params
135 | * @return Model
136 | * @throws InvalidArgumentException
137 | */
138 | public function validate(Model $model, array $params): Model
139 | {
140 | $model->load($params, '');
141 | if (!$model->validate()) {
142 | throw new InvalidArgumentException(Setup::errorMessage($model->firstErrors));
143 | }
144 | return $model;
145 | }
146 |
147 | /**
148 | * @param string $action
149 | * @param null $model
150 | * @param array $params
151 | * @throws ForbiddenHttpException
152 | */
153 | public function checkAccess($action, $model = null, $params = [])
154 | {
155 | if (in_array($action, ['delete', 'update', 'view'])) {
156 | if ($model->user_id !== \Yii::$app->user->id) {
157 | throw new ForbiddenHttpException(
158 | t('app', 'You can only ' . $action . ' data that you\'ve created.')
159 | );
160 | }
161 | }
162 | }
163 | }
164 |
--------------------------------------------------------------------------------
/modules/v1/controllers/AnalysisController.php:
--------------------------------------------------------------------------------
1 | request->queryParams;
31 | return $this->analysisService->byCategory($params);
32 | }
33 |
34 | /**
35 | * @return array
36 | * @throws InvalidArgumentException
37 | */
38 | public function actionDate()
39 | {
40 | $params = \Yii::$app->request->queryParams;
41 | $groupByDateType = request('group_type') ?: AnalysisGroupDateType::DAY;
42 | return $this->analysisService->byDate($params, AnalysisGroupDateType::getValue($groupByDateType));
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/modules/v1/controllers/CategoryController.php:
--------------------------------------------------------------------------------
1 | SORT_ASC, 'id' => SORT_DESC];
21 | public $partialMatchAttributes = ['name'];
22 | public $stringToIntAttributes = ['transaction_type' => TransactionType::class];
23 |
24 | /**
25 | * @return array
26 | * @throws InvalidArgumentException
27 | * @throws \Exception
28 | */
29 | public function actionAnalysis()
30 | {
31 | $transactionType = request('transaction_type', TransactionType::getName(TransactionType::EXPENSE));
32 | $dateType = request('date_type', AnalysisDateType::CURRENT_MONTH);
33 | $date = AnalysisService::getDateRange($dateType);
34 |
35 | return $this->analysisService->getCategoryStatisticalData(
36 | $date,
37 | TransactionType::toEnumValue($transactionType)
38 | );
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/modules/v1/controllers/RecordController.php:
--------------------------------------------------------------------------------
1 | SORT_DESC, 'id' => SORT_DESC];
27 | public $stringToIntAttributes = ['transaction_type' => TransactionType::class];
28 | public $relations = ['transaction' => []];
29 |
30 | public function actions()
31 | {
32 | $actions = parent::actions();
33 | unset($actions['create'], $actions['update']);
34 | return $actions;
35 | }
36 |
37 |
38 | /**
39 | * @return ActiveDataProvider
40 | * @throws InvalidArgumentException
41 | * @throws InvalidConfigException
42 | * @throws InternalException
43 | */
44 | public function prepareDataProvider()
45 | {
46 | $dataProvider = parent::prepareDataProvider();
47 | if ($searchKeywords = trim(request('keyword'))) {
48 | $dataProvider->query->andWhere(
49 | "MATCH(`description`, `tags`, `remark`) AGAINST ('*$searchKeywords*' IN BOOLEAN MODE)"
50 | );
51 | }
52 | $dataProvider->setModels($this->transactionService->formatRecords($dataProvider->getModels()));
53 | return $dataProvider;
54 | }
55 |
56 | /**
57 | * @param array $params
58 | * @return array
59 | * @throws Exception
60 | */
61 | protected function formatParams(array $params)
62 | {
63 | if (($date = explode('~', data_get($params, 'date'))) && count($date) == 2) {
64 | $start = $date[0] . ' 00:00:00';
65 | $end = $date[1] . ' 23:59:59';
66 | $params['date'] = "{$start}~{$end}";
67 | }
68 | return $params;
69 | }
70 |
71 | /**
72 | * @return array
73 | * @throws Exception
74 | */
75 | public function actionOverview()
76 | {
77 | return array_values($this->analysisService->recordOverview);
78 | }
79 |
80 |
81 | /**
82 | * @return array
83 | * @throws InvalidArgumentException
84 | * @throws Exception
85 | */
86 | public function actionAnalysis()
87 | {
88 | $transactionType = request('transaction_type', TransactionType::getName(TransactionType::EXPENSE));
89 | $date = request('date', Yii::$app->formatter->asDatetime('now'));
90 |
91 | return $this->analysisService->getRecordStatisticalData(
92 | $date,
93 | TransactionType::toEnumValue($transactionType)
94 | );
95 | }
96 |
97 | /**
98 | * @return array
99 | * @throws Exception
100 | */
101 | public function actionSources()
102 | {
103 | $items = [];
104 | $names = RecordSource::names();
105 | foreach ($names as $key => $name) {
106 | $items[] = ['type' => $key, 'name' => $name];
107 | }
108 | return $items;
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/modules/v1/controllers/RecurrenceController.php:
--------------------------------------------------------------------------------
1 | request->bodyParams;
36 | $model = new RecurrenceUpdateStatusRequest();
37 | /** @var RecurrenceUpdateStatusRequest $model */
38 | $model = $this->validate($model, $params);
39 |
40 | return $this->recurrenceService->updateStatus($id, $model->status);
41 | }
42 |
43 |
44 | /**
45 | * @return array
46 | * @throws \Exception
47 | */
48 | public function actionFrequencyTypes()
49 | {
50 | $items = [];
51 | $texts = RecurrenceFrequency::texts();
52 | foreach (RecurrenceFrequency::names() as $key => $name) {
53 | $items[] = ['type' => $name, 'name' => data_get($texts, $key)];
54 | }
55 | return $items;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/modules/v1/controllers/RuleController.php:
--------------------------------------------------------------------------------
1 | SORT_ASC, 'id' => SORT_DESC];
22 | public $partialMatchAttributes = ['name'];
23 |
24 | /**
25 | * @param int $id
26 | * @return Rule
27 | * @throws Exception
28 | * @throws NotFoundHttpException
29 | */
30 | public function actionCopy(int $id): Rule
31 | {
32 | return $this->ruleService->copy($id);
33 | }
34 |
35 | /**
36 | * @param int $id
37 | * @return Rule
38 | * @throws Exception
39 | * @throws InvalidArgumentException
40 | * @throws NotFoundHttpException
41 | */
42 | public function actionUpdateStatus(int $id): Rule
43 | {
44 | $params = Yii::$app->request->bodyParams;
45 | $model = new RuleUpdateStatusRequest();
46 | /** @var RuleUpdateStatusRequest $model */
47 | $model = $this->validate($model, $params);
48 |
49 | return $this->ruleService->updateStatus($id, $model->status);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/modules/v1/controllers/TagController.php:
--------------------------------------------------------------------------------
1 | SORT_DESC, 'id' => SORT_DESC];
14 | public $partialMatchAttributes = ['name'];
15 | }
16 |
--------------------------------------------------------------------------------
/modules/v1/controllers/TransactionController.php:
--------------------------------------------------------------------------------
1 | request->bodyParams;
38 | $model = new TransactionCreateByDescRequest();
39 | /** @var TransactionCreateByDescRequest $model */
40 | $model = $this->validate($model, $params);
41 |
42 | return $this->transactionService->createByDesc($model->description);
43 | }
44 |
45 | /**
46 | * @return array
47 | * @throws \Exception
48 | */
49 | public function actionTypes()
50 | {
51 | $items = [];
52 | $texts = TransactionType::texts();
53 | $names = TransactionType::names();
54 | // unset($names[TransactionType::ADJUST]);
55 | foreach ($names as $key => $name) {
56 | $items[] = ['type' => $name, 'name' => data_get($texts, $key)];
57 | }
58 | return $items;
59 | }
60 |
61 |
62 | /**
63 | * @return array
64 | * @throws InvalidArgumentException
65 | * @throws \Exception
66 | */
67 | public function actionUpload()
68 | {
69 | $fileParam = 'file';
70 | $uploadedFile = UploadedFile::getInstanceByName($fileParam);
71 | $params = [$fileParam => $uploadedFile];
72 | $model = new TransactionUploadRequest();
73 | $this->validate($model, $params);
74 | $filename = Yii::$app->user->id . 'record.csv';
75 | $this->uploadService->uploadRecord($uploadedFile, $filename);
76 | $data = $this->transactionService->createByCSV($filename);
77 | $this->uploadService->deleteLocalFile($filename);
78 | return $data;
79 | }
80 |
81 |
82 | /**
83 | * @return array
84 | * @throws \Exception
85 | */
86 | public function actionExport()
87 | {
88 | return $this->transactionService->exportData();
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/modules/v1/controllers/UserController.php:
--------------------------------------------------------------------------------
1 | request->bodyParams;
39 | $data = $this->validate(new JoinRequest(), $params);
40 |
41 | /** @var JoinRequest $data */
42 | return $this->userService->createUser($data);
43 | }
44 |
45 | /**
46 | * @return string[]
47 | * @throws InvalidArgumentException|\Throwable
48 | */
49 | public function actionLogin()
50 | {
51 | $params = Yii::$app->request->bodyParams;
52 | $this->validate(new LoginRequest(), $params);
53 | $token = $this->userService->getToken();
54 | $user = Yii::$app->user->identity;
55 |
56 | return [
57 | 'user' => $user,
58 | 'token' => (string)$token,
59 | ];
60 | }
61 |
62 | public function actionRefreshToken()
63 | {
64 | $user = Yii::$app->user->identity;
65 | $token = $this->userService->getToken();
66 | return [
67 | 'user' => $user,
68 | 'token' => (string)$token,
69 | ];
70 | }
71 |
72 | /**
73 | * @return array
74 | * @throws \yii\base\Exception
75 | */
76 | public function actionResetToken()
77 | {
78 | /** @var User $user */
79 | $user = Yii::$app->user->identity;
80 | $this->userService->setPasswordResetToken($user);
81 | return [
82 | 'reset_token' => $user->password_reset_token,
83 | 'expire_in' => params('user.passwordResetTokenExpire')
84 | ];
85 | }
86 |
87 | /**
88 | * @return array
89 | */
90 | public function actionGetAuthClients()
91 | {
92 | return $this->userService->getAuthClients();
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/requirements.php:
--------------------------------------------------------------------------------
1 | Error\n\n"
34 | . "The path to yii framework seems to be incorrect.
\n"
35 | . 'You need to install Yii framework via composer or adjust the framework path in file ' . basename(__FILE__) . ".
\n"
36 | . 'Please refer to the README on how to install Yii.
\n";
37 |
38 | if (!empty($_SERVER['argv'])) {
39 | // do not print HTML when used in console mode
40 | echo strip_tags($message);
41 | } else {
42 | echo $message;
43 | }
44 | exit(1);
45 | }
46 |
47 | require_once($frameworkPath . '/requirements/YiiRequirementChecker.php');
48 | $requirementsChecker = new YiiRequirementChecker();
49 |
50 | $gdMemo = $imagickMemo = 'Either GD PHP extension with FreeType support or ImageMagick PHP extension with PNG support is required for image CAPTCHA.';
51 | $gdOK = $imagickOK = false;
52 |
53 | if (extension_loaded('imagick')) {
54 | $imagick = new Imagick();
55 | $imagickFormats = $imagick->queryFormats('PNG');
56 | if (in_array('PNG', $imagickFormats)) {
57 | $imagickOK = true;
58 | } else {
59 | $imagickMemo = 'Imagick extension should be installed with PNG support in order to be used for image CAPTCHA.';
60 | }
61 | }
62 |
63 | if (extension_loaded('gd')) {
64 | $gdInfo = gd_info();
65 | if (!empty($gdInfo['FreeType Support'])) {
66 | $gdOK = true;
67 | } else {
68 | $gdMemo = 'GD extension should be installed with FreeType support in order to be used for image CAPTCHA.';
69 | }
70 | }
71 |
72 | /**
73 | * Adjust requirements according to your application specifics.
74 | */
75 | $requirements = array(
76 | // Database :
77 | array(
78 | 'name' => 'PDO extension',
79 | 'mandatory' => true,
80 | 'condition' => extension_loaded('pdo'),
81 | 'by' => 'All DB-related classes',
82 | ),
83 | array(
84 | 'name' => 'PDO SQLite extension',
85 | 'mandatory' => false,
86 | 'condition' => extension_loaded('pdo_sqlite'),
87 | 'by' => 'All DB-related classes',
88 | 'memo' => 'Required for SQLite database.',
89 | ),
90 | array(
91 | 'name' => 'PDO MySQL extension',
92 | 'mandatory' => false,
93 | 'condition' => extension_loaded('pdo_mysql'),
94 | 'by' => 'All DB-related classes',
95 | 'memo' => 'Required for MySQL database.',
96 | ),
97 | array(
98 | 'name' => 'PDO PostgreSQL extension',
99 | 'mandatory' => false,
100 | 'condition' => extension_loaded('pdo_pgsql'),
101 | 'by' => 'All DB-related classes',
102 | 'memo' => 'Required for PostgreSQL database.',
103 | ),
104 | // Cache :
105 | array(
106 | 'name' => 'Memcache extension',
107 | 'mandatory' => false,
108 | 'condition' => extension_loaded('memcache') || extension_loaded('memcached'),
109 | 'by' => 'MemCache',
110 | 'memo' => extension_loaded('memcached') ? 'To use memcached set MemCache::useMemcached to true
.' : ''
111 | ),
112 | // CAPTCHA:
113 | array(
114 | 'name' => 'GD PHP extension with FreeType support',
115 | 'mandatory' => false,
116 | 'condition' => $gdOK,
117 | 'by' => 'Captcha',
118 | 'memo' => $gdMemo,
119 | ),
120 | array(
121 | 'name' => 'ImageMagick PHP extension with PNG support',
122 | 'mandatory' => false,
123 | 'condition' => $imagickOK,
124 | 'by' => 'Captcha',
125 | 'memo' => $imagickMemo,
126 | ),
127 | // PHP ini :
128 | 'phpExposePhp' => array(
129 | 'name' => 'Expose PHP',
130 | 'mandatory' => false,
131 | 'condition' => $requirementsChecker->checkPhpIniOff("expose_php"),
132 | 'by' => 'Security reasons',
133 | 'memo' => '"expose_php" should be disabled at php.ini',
134 | ),
135 | 'phpAllowUrlInclude' => array(
136 | 'name' => 'PHP allow url include',
137 | 'mandatory' => false,
138 | 'condition' => $requirementsChecker->checkPhpIniOff("allow_url_include"),
139 | 'by' => 'Security reasons',
140 | 'memo' => '"allow_url_include" should be disabled at php.ini',
141 | ),
142 | 'phpSmtp' => array(
143 | 'name' => 'PHP mail SMTP',
144 | 'mandatory' => false,
145 | 'condition' => strlen(ini_get('SMTP')) > 0,
146 | 'by' => 'Email sending',
147 | 'memo' => 'PHP mail SMTP server required',
148 | ),
149 | );
150 |
151 | // OPcache check
152 | if (!version_compare(phpversion(), '5.5', '>=')) {
153 | $requirements[] = array(
154 | 'name' => 'APC extension',
155 | 'mandatory' => false,
156 | 'condition' => extension_loaded('apc'),
157 | 'by' => 'ApcCache',
158 | );
159 | }
160 |
161 | $result = $requirementsChecker->checkYii()->check($requirements)->getResult();
162 | $requirementsChecker->render();
163 | exit($result['summary']['errors'] === 0 ? 0 : 1);
164 |
--------------------------------------------------------------------------------
/runtime/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
--------------------------------------------------------------------------------
/tests/_bootstrap.php:
--------------------------------------------------------------------------------
1 | [
29 | 'username' => 'demo',
30 | 'email' => 'demo@yii.com',
31 | 'password' => 'pass123',
32 | 'base_currency_code' => 'CNS',
33 | ],
34 | 'code' => ErrorCodes::INVALID_ARGUMENT_ERROR
35 | ],
36 | [
37 | 'data' => [
38 | 'username' => 'demo',
39 | 'email' => 'demo@yii.com',
40 | 'password' => 'pass123',
41 | ],
42 | 'code' => ErrorCodes::INVALID_ARGUMENT_ERROR
43 | ],
44 | [
45 | 'data' => [
46 | 'username' => 'demo',
47 | 'email' => 'demo',
48 | 'password' => 'pass123',
49 | ],
50 | 'code' => ErrorCodes::INVALID_ARGUMENT_ERROR
51 | ],
52 | [
53 | 'data' => [
54 | 'username' => 'demo-sdsdkj',
55 | 'email' => 'demo@yii.com',
56 | 'password' => 'pass123',
57 | ],
58 | 'code' => ErrorCodes::INVALID_ARGUMENT_ERROR
59 | ],
60 | [
61 | 'data' => [
62 | 'username' => 'demo-sdsdkj',
63 | 'email' => 'demo@yii.com',
64 | 'password' => 'pass1',
65 | ],
66 | 'code' => ErrorCodes::INVALID_ARGUMENT_ERROR
67 | ],
68 | [
69 | 'data' => [
70 | 'username' => 'demo-sdsdkj',
71 | 'password' => 'pass1',
72 | ],
73 | 'code' => ErrorCodes::INVALID_ARGUMENT_ERROR
74 | ],
75 | ];
76 | }
77 |
78 | /**
79 | * @dataProvider makeValidateFailItems
80 | * @param ApiTester $I
81 | * @param Example $example
82 | */
83 | public function createUserViaAPIFail(ApiTester $I, Example $example)
84 | {
85 | $I->haveHttpHeader('content-type', 'application/json');
86 | $I->sendPOST('/join', $example['data']);
87 | $I->seeResponseCodeIs(HttpCode::OK); // 200
88 | $I->seeResponseIsJson();
89 | $I->seeResponseContainsJson(['code' => $example['code']]);
90 | }
91 |
92 | /**
93 | * @param ApiTester $I
94 | */
95 | public function createUser(ApiTester $I)
96 | {
97 | $I->haveHttpHeader('content-type', 'application/json');
98 | $I->sendPOST('/join', [
99 | 'username' => 'demo',
100 | 'email' => 'demo@yii.com',
101 | 'password' => 'pass123',
102 | 'base_currency_code' => 'CNY',
103 | ]);
104 | $I->seeResponseCodeIs(HttpCode::OK); // 200
105 | $I->seeResponseIsJson();
106 | $I->seeResponseContainsJson(['code' => 0]);
107 | $I->seeResponseJsonMatchesXpath('//data/username');
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/tests/api/LoginUserCest.php:
--------------------------------------------------------------------------------
1 | haveHttpHeader('content-type', 'application/json');
27 | $I->sendPOST('/login', [
28 | 'username' => 'demo',
29 | 'password' => 'pass123456',
30 | ]);
31 | $I->seeResponseCodeIs(HttpCode::OK); // 200
32 | $I->seeResponseIsJson();
33 | $I->seeResponseContainsJson(['code' => ErrorCodes::INVALID_ARGUMENT_ERROR]);
34 | }
35 |
36 |
37 | /**
38 | * @param ApiTester $I
39 | * @param CreateUserCest $createUserCest
40 | */
41 | public function userLogin(ApiTester $I, CreateUserCest $createUserCest)
42 | {
43 | $createUserCest->createUser($I);
44 | $I->haveHttpHeader('content-type', 'application/json');
45 | $I->sendPOST('/login', [
46 | 'username' => 'demo',
47 | 'password' => 'pass123',
48 | ]);
49 | $I->seeResponseCodeIs(HttpCode::OK); // 200
50 | $I->seeResponseIsJson();
51 | $I->seeResponseContainsJson(['code' => 0]);
52 | $I->seeResponseJsonMatchesXpath('//data/token');
53 | $I->seeResponseJsonMatchesXpath('//data/user/username');
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/web/.htaccess:
--------------------------------------------------------------------------------
1 |
2 | SetEnvIf Authorization .+ HTTP_AUTHORIZATION=$0
3 |
4 | RewriteEngine on
5 |
6 | # Authorization Headers
7 | #RewriteCond %{HTTP:Authorization} .
8 | #RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
9 |
10 | RewriteCond %{REQUEST_FILENAME} !-f
11 | RewriteCond %{REQUEST_FILENAME} !-d
12 |
13 | RewriteRule . index.php
14 |
--------------------------------------------------------------------------------
/web/assets/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/web/index.php:
--------------------------------------------------------------------------------
1 | run();
12 |
--------------------------------------------------------------------------------
/web/uploads/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 | !import-template.csv
4 |
--------------------------------------------------------------------------------
/web/uploads/import-template.csv:
--------------------------------------------------------------------------------
1 | 账单日期,类别,类型,金额,标签,描述,备注,账户1,账户2
2 | 2020-07-31,餐饮食品,支出,10,早餐/三餐,昨天晚餐10,示例数据,,
3 | 2020-07-31,转账,转账,15,,,示例数据,测试账户,测试
4 |
--------------------------------------------------------------------------------
/yii:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | run();
22 | exit($exitCode);
23 |
--------------------------------------------------------------------------------
/yii.bat:
--------------------------------------------------------------------------------
1 | @echo off
2 |
3 | rem -------------------------------------------------------------
4 | rem Yii command line bootstrap script for Windows.
5 | rem
6 | rem @author Qiang Xue
7 | rem @link http://www.yiiframework.com/
8 | rem @copyright Copyright (c) 2008 Yii Software LLC
9 | rem @license http://www.yiiframework.com/license/
10 | rem -------------------------------------------------------------
11 |
12 | @setlocal
13 |
14 | set YII_PATH=%~dp0
15 |
16 | if "%PHP_COMMAND%" == "" set PHP_COMMAND=php.exe
17 |
18 | "%PHP_COMMAND%" "%YII_PATH%yii" %*
19 |
20 | @endlocal
21 |
--------------------------------------------------------------------------------