├── public
├── favicon.ico
├── robots.txt
├── assets
│ ├── style
│ │ └── favicon.ico
│ └── common
│ │ └── images
│ │ ├── 3dian.png
│ │ ├── android.png
│ │ ├── banner.png
│ │ ├── default.jpg
│ │ └── iphone.png
├── fonts
│ └── filament
│ │ └── filament
│ │ └── inter
│ │ ├── inter-greek-wght-normal-AXVTPQD5.woff2
│ │ ├── inter-greek-wght-normal-IRE366VL.woff2
│ │ ├── inter-greek-wght-normal-N43DBLU2.woff2
│ │ ├── inter-latin-wght-normal-NRMW37G5.woff2
│ │ ├── inter-latin-wght-normal-O25CN4JL.woff2
│ │ ├── inter-latin-wght-normal-OPIJAQLS.woff2
│ │ ├── inter-cyrillic-wght-normal-EWLSKVKN.woff2
│ │ ├── inter-cyrillic-wght-normal-JEOLYBOO.woff2
│ │ ├── inter-cyrillic-wght-normal-R5CMSONN.woff2
│ │ ├── inter-greek-ext-wght-normal-7GGTF7EK.woff2
│ │ ├── inter-greek-ext-wght-normal-EOVOK2B5.woff2
│ │ ├── inter-greek-ext-wght-normal-ZEVLMORV.woff2
│ │ ├── inter-latin-ext-wght-normal-5SRY4DMZ.woff2
│ │ ├── inter-latin-ext-wght-normal-GZCIV3NH.woff2
│ │ ├── inter-latin-ext-wght-normal-HA22NDSG.woff2
│ │ ├── inter-vietnamese-wght-normal-CE5GGD3W.woff2
│ │ ├── inter-vietnamese-wght-normal-TWG5UU7E.woff2
│ │ ├── inter-cyrillic-ext-wght-normal-ASVAGXXE.woff2
│ │ ├── inter-cyrillic-ext-wght-normal-IYF56FF6.woff2
│ │ ├── inter-cyrillic-ext-wght-normal-XKHXBTUO.woff2
│ │ └── index.css
├── js
│ ├── marcelweidum
│ │ └── filament-expiration-notice
│ │ │ └── filament-expiration-notice-scripts.js
│ └── filament
│ │ ├── forms
│ │ └── components
│ │ │ ├── textarea.js
│ │ │ ├── tags-input.js
│ │ │ ├── key-value.js
│ │ │ └── checkbox-list.js
│ │ ├── schemas
│ │ └── components
│ │ │ ├── tabs.js
│ │ │ ├── actions.js
│ │ │ └── wizard.js
│ │ ├── tables
│ │ └── components
│ │ │ └── columns
│ │ │ ├── checkbox.js
│ │ │ ├── toggle.js
│ │ │ └── text-input.js
│ │ └── actions
│ │ └── actions.js
├── images
│ └── placeholder.svg
├── build
│ └── manifest.json
├── index.php
└── .htaccess
├── database
├── .gitignore
├── seeders
│ ├── DatabaseSeeder.php
│ └── SystemSettingSeeder.php
├── migrations
│ ├── 2025_11_01_013553_create_coupons_goods_table.php
│ ├── 2025_11_02_120443_create_notifications_table.php
│ ├── 2025_11_02_120452_create_failed_import_rows_table.php
│ ├── 2025_11_01_013547_create_goods_group_table.php
│ ├── 2025_11_01_013554_create_emailtpls_table.php
│ ├── 0001_01_01_000001_create_cache_table.php
│ ├── 2025_11_01_013550_create_carmis_table.php
│ ├── 2025_11_02_120450_create_imports_table.php
│ ├── 2025_11_01_013552_create_coupons_table.php
│ ├── 2025_11_02_120451_create_exports_table.php
│ ├── 2025_11_01_013555_create_pays_table.php
│ ├── 0001_01_01_000000_create_users_table.php
│ ├── 0001_01_01_000002_create_jobs_table.php
│ ├── 2025_11_01_013549_create_goods_table.php
│ └── 2025_11_01_013557_create_orders_table.php
└── factories
│ ├── GoodsGroupFactory.php
│ ├── UserFactory.php
│ ├── CarmisFactory.php
│ └── CouponFactory.php
├── bootstrap
├── cache
│ └── .gitignore
├── providers.php
└── app.php
├── resources
├── js
│ ├── app.js
│ └── bootstrap.js
├── views
│ ├── email
│ │ └── mail.blade.php
│ ├── filament
│ │ └── pages
│ │ │ ├── email-test.blade.php
│ │ │ ├── import-carmis.blade.php
│ │ │ └── system-setting.blade.php
│ └── vendor
│ │ └── geetest
│ │ └── geetest.blade.php
├── lang
│ ├── vendor
│ │ └── filament
│ │ │ ├── en
│ │ │ └── components
│ │ │ │ ├── copyable.php
│ │ │ │ ├── button.php
│ │ │ │ ├── modal.php
│ │ │ │ └── pagination.php
│ │ │ ├── zh_CN
│ │ │ └── components
│ │ │ │ ├── copyable.php
│ │ │ │ ├── button.php
│ │ │ │ ├── modal.php
│ │ │ │ └── pagination.php
│ │ │ └── zh_TW
│ │ │ └── components
│ │ │ ├── copyable.php
│ │ │ ├── button.php
│ │ │ ├── modal.php
│ │ │ └── pagination.php
│ ├── zh_CN
│ │ ├── emailtpl.php
│ │ ├── goods-group.php
│ │ ├── email-test.php
│ │ ├── coupon.php
│ │ ├── pay.php
│ │ ├── carmis.php
│ │ ├── global.php
│ │ ├── order.php
│ │ ├── goods.php
│ │ └── system-setting.php
│ ├── zh_TW
│ │ ├── emailtpl.php
│ │ ├── goods-group.php
│ │ ├── coupon.php
│ │ ├── pay.php
│ │ ├── carmis.php
│ │ ├── global.php
│ │ ├── order.php
│ │ ├── goods.php
│ │ └── system-setting.php
│ └── en
│ │ ├── pagination.php
│ │ ├── auth.php
│ │ └── passwords.php
└── css
│ ├── filament
│ └── admin
│ │ └── theme.css
│ └── app.css
├── storage
├── logs
│ └── .gitignore
├── app
│ ├── private
│ │ └── .gitignore
│ ├── public
│ │ └── .gitignore
│ └── .gitignore
└── framework
│ ├── testing
│ └── .gitignore
│ ├── views
│ └── .gitignore
│ ├── cache
│ ├── data
│ │ └── .gitignore
│ └── .gitignore
│ ├── sessions
│ └── .gitignore
│ └── .gitignore
├── art
├── web-buy.png
├── web-dark.png
├── web-index.png
├── web-order.png
├── admin-carmis.png
├── admin-goods.png
├── admin-orders.png
├── admin-search.png
├── admin-coupons.png
├── admin-dashboard.png
├── web-search-order.png
├── admin-goods-groups.png
└── admin-system-setting.png
├── .mcp.json
├── .cursor
└── mcp.json
├── .gitattributes
├── config
├── payjs.php
├── dujiaoka.php
├── services.php
├── view.php
├── dujiaoka_settings.php
├── hashing.php
└── broadcasting.php
├── boost.json
├── tests
├── TestCase.php
├── Pest.php
└── Feature
│ ├── LivewirePagesTest.php
│ └── SystemHealthTest.php
├── routes
├── web.php
├── console.php
└── common
│ └── web.php
├── .editorconfig
├── phpstan.neon.dist
├── .gitignore
├── app
├── Exceptions
│ ├── AppException.php
│ └── RuleValidationException.php
├── Filament
│ ├── Resources
│ │ ├── Pays
│ │ │ ├── Pages
│ │ │ │ ├── CreatePay.php
│ │ │ │ ├── ListPays.php
│ │ │ │ └── EditPay.php
│ │ │ └── PayResource.php
│ │ ├── Goods
│ │ │ └── Pages
│ │ │ │ ├── CreateGoods.php
│ │ │ │ ├── EditGoods.php
│ │ │ │ └── ListGoods.php
│ │ ├── Carmis
│ │ │ ├── Pages
│ │ │ │ ├── CreateCarmis.php
│ │ │ │ ├── ListCarmis.php
│ │ │ │ └── EditCarmis.php
│ │ │ └── CarmisResource.php
│ │ ├── Orders
│ │ │ ├── Pages
│ │ │ │ ├── CreateOrder.php
│ │ │ │ ├── EditOrder.php
│ │ │ │ └── ListOrders.php
│ │ │ └── Widgets
│ │ │ │ └── OrderStatsOverview.php
│ │ ├── Coupons
│ │ │ ├── Pages
│ │ │ │ ├── CreateCoupon.php
│ │ │ │ ├── ListCoupons.php
│ │ │ │ └── EditCoupon.php
│ │ │ └── Schemas
│ │ │ │ └── CouponForm.php
│ │ ├── Emailtpls
│ │ │ ├── Pages
│ │ │ │ ├── CreateEmailtpl.php
│ │ │ │ ├── ListEmailtpls.php
│ │ │ │ └── EditEmailtpl.php
│ │ │ ├── EmailtplResource.php
│ │ │ ├── Tables
│ │ │ │ └── EmailtplsTable.php
│ │ │ └── Schemas
│ │ │ │ └── EmailtplForm.php
│ │ └── GoodsGroups
│ │ │ ├── Pages
│ │ │ ├── CreateGoodsGroup.php
│ │ │ ├── ListGoodsGroups.php
│ │ │ └── EditGoodsGroup.php
│ │ │ ├── Schemas
│ │ │ └── GoodsGroupForm.php
│ │ │ └── GoodsGroupResource.php
│ ├── Exports
│ │ └── CarmisExporter.php
│ └── Imports
│ │ └── CarmisImporter.php
├── Http
│ ├── Middleware
│ │ ├── PayGateWay.php
│ │ ├── DujiaoSystem.php
│ │ └── DujiaoBoot.php
│ └── Controllers
│ │ ├── Controller.php
│ │ ├── BaseController.php
│ │ ├── Home
│ │ └── HomeController.php
│ │ └── Pay
│ │ └── PayjsController.php
├── Models
│ ├── Emailtpl.php
│ ├── GoodsGroup.php
│ ├── BaseModel.php
│ ├── User.php
│ ├── Coupon.php
│ ├── Carmis.php
│ └── Pay.php
├── Service
│ ├── EmailtplService.php
│ ├── CarmisService.php
│ └── CouponService.php
├── Listeners
│ ├── GoodsGroupDeleted.php
│ └── GoodsDeleted.php
├── Rules
│ ├── SearchPwd.php
│ └── VerifyImg.php
├── Events
│ ├── GoodsDeleted.php
│ ├── OrderUpdated.php
│ └── GoodsGroupDeleted.php
├── Jobs
│ ├── CouponBack.php
│ ├── OrderExpired.php
│ ├── ServerJiang.php
│ ├── ApiHook.php
│ └── MailSend.php
├── Providers
│ └── EventServiceProvider.php
└── Livewire
│ └── Pages
│ ├── OrderInfo.php
│ ├── Home.php
│ └── QrPay.php
├── vite.config.js
├── pint.json
├── package.json
├── artisan
├── rector.php
├── docs
└── development-logs
│ └── README.md
├── phpunit.xml
└── .env.example
/public/favicon.ico:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/database/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite*
2 |
--------------------------------------------------------------------------------
/bootstrap/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/resources/js/app.js:
--------------------------------------------------------------------------------
1 | import './bootstrap';
2 |
--------------------------------------------------------------------------------
/storage/logs/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/storage/app/private/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/app/public/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/resources/views/email/mail.blade.php:
--------------------------------------------------------------------------------
1 | {!! $body !!}
2 |
--------------------------------------------------------------------------------
/storage/framework/testing/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/views/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/cache/data/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/sessions/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !data/
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/storage/app/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !private/
3 | !public/
4 | !.gitignore
5 |
--------------------------------------------------------------------------------
/art/web-buy.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/art/web-buy.png
--------------------------------------------------------------------------------
/art/web-dark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/art/web-dark.png
--------------------------------------------------------------------------------
/art/web-index.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/art/web-index.png
--------------------------------------------------------------------------------
/art/web-order.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/art/web-order.png
--------------------------------------------------------------------------------
/art/admin-carmis.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/art/admin-carmis.png
--------------------------------------------------------------------------------
/art/admin-goods.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/art/admin-goods.png
--------------------------------------------------------------------------------
/art/admin-orders.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/art/admin-orders.png
--------------------------------------------------------------------------------
/art/admin-search.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/art/admin-search.png
--------------------------------------------------------------------------------
/art/admin-coupons.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/art/admin-coupons.png
--------------------------------------------------------------------------------
/art/admin-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/art/admin-dashboard.png
--------------------------------------------------------------------------------
/art/web-search-order.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/art/web-search-order.png
--------------------------------------------------------------------------------
/art/admin-goods-groups.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/art/admin-goods-groups.png
--------------------------------------------------------------------------------
/art/admin-system-setting.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/art/admin-system-setting.png
--------------------------------------------------------------------------------
/public/assets/style/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/assets/style/favicon.ico
--------------------------------------------------------------------------------
/public/assets/common/images/3dian.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/assets/common/images/3dian.png
--------------------------------------------------------------------------------
/public/assets/common/images/android.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/assets/common/images/android.png
--------------------------------------------------------------------------------
/public/assets/common/images/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/assets/common/images/banner.png
--------------------------------------------------------------------------------
/public/assets/common/images/default.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/assets/common/images/default.jpg
--------------------------------------------------------------------------------
/public/assets/common/images/iphone.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/assets/common/images/iphone.png
--------------------------------------------------------------------------------
/resources/js/bootstrap.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | window.axios = axios;
3 |
4 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
5 |
--------------------------------------------------------------------------------
/resources/lang/vendor/filament/en/components/copyable.php:
--------------------------------------------------------------------------------
1 | [
6 | 'copied' => 'Copied',
7 | ],
8 |
9 | ];
10 |
--------------------------------------------------------------------------------
/resources/lang/vendor/filament/zh_CN/components/copyable.php:
--------------------------------------------------------------------------------
1 | [
6 | 'copied' => '已复制',
7 | ],
8 |
9 | ];
10 |
--------------------------------------------------------------------------------
/resources/lang/vendor/filament/zh_TW/components/copyable.php:
--------------------------------------------------------------------------------
1 | [
6 | 'copied' => '已複製',
7 | ],
8 |
9 | ];
10 |
--------------------------------------------------------------------------------
/resources/lang/vendor/filament/zh_CN/components/button.php:
--------------------------------------------------------------------------------
1 | [
6 | 'uploading_file' => '文件上传中...',
7 | ],
8 |
9 | ];
10 |
--------------------------------------------------------------------------------
/resources/lang/vendor/filament/zh_TW/components/button.php:
--------------------------------------------------------------------------------
1 | [
6 | 'uploading_file' => '正在上傳檔案...',
7 | ],
8 |
9 | ];
10 |
--------------------------------------------------------------------------------
/storage/framework/.gitignore:
--------------------------------------------------------------------------------
1 | compiled.php
2 | config.php
3 | down
4 | events.scanned.php
5 | maintenance.php
6 | routes.php
7 | routes.scanned.php
8 | schedule-*
9 | services.json
10 |
--------------------------------------------------------------------------------
/resources/lang/vendor/filament/en/components/button.php:
--------------------------------------------------------------------------------
1 | [
6 | 'uploading_file' => 'Uploading file...',
7 | ],
8 |
9 | ];
10 |
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-greek-wght-normal-AXVTPQD5.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-greek-wght-normal-AXVTPQD5.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-greek-wght-normal-IRE366VL.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-greek-wght-normal-IRE366VL.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-greek-wght-normal-N43DBLU2.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-greek-wght-normal-N43DBLU2.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-latin-wght-normal-NRMW37G5.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-latin-wght-normal-NRMW37G5.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-latin-wght-normal-O25CN4JL.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-latin-wght-normal-O25CN4JL.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-latin-wght-normal-OPIJAQLS.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-latin-wght-normal-OPIJAQLS.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-cyrillic-wght-normal-EWLSKVKN.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-cyrillic-wght-normal-EWLSKVKN.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-cyrillic-wght-normal-JEOLYBOO.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-cyrillic-wght-normal-JEOLYBOO.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-cyrillic-wght-normal-R5CMSONN.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-cyrillic-wght-normal-R5CMSONN.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-greek-ext-wght-normal-7GGTF7EK.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-greek-ext-wght-normal-7GGTF7EK.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-greek-ext-wght-normal-EOVOK2B5.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-greek-ext-wght-normal-EOVOK2B5.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-greek-ext-wght-normal-ZEVLMORV.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-greek-ext-wght-normal-ZEVLMORV.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-latin-ext-wght-normal-5SRY4DMZ.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-latin-ext-wght-normal-5SRY4DMZ.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-latin-ext-wght-normal-GZCIV3NH.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-latin-ext-wght-normal-GZCIV3NH.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-latin-ext-wght-normal-HA22NDSG.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-latin-ext-wght-normal-HA22NDSG.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-vietnamese-wght-normal-CE5GGD3W.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-vietnamese-wght-normal-CE5GGD3W.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-vietnamese-wght-normal-TWG5UU7E.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-vietnamese-wght-normal-TWG5UU7E.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-cyrillic-ext-wght-normal-ASVAGXXE.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-cyrillic-ext-wght-normal-ASVAGXXE.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-cyrillic-ext-wght-normal-IYF56FF6.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-cyrillic-ext-wght-normal-IYF56FF6.woff2
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/inter-cyrillic-ext-wght-normal-XKHXBTUO.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/myxiaoao/dujiaoka-next/HEAD/public/fonts/filament/filament/inter/inter-cyrillic-ext-wght-normal-XKHXBTUO.woff2
--------------------------------------------------------------------------------
/resources/lang/vendor/filament/en/components/modal.php:
--------------------------------------------------------------------------------
1 | [
6 |
7 | 'close' => [
8 | 'label' => 'Close',
9 | ],
10 |
11 | ],
12 |
13 | ];
14 |
--------------------------------------------------------------------------------
/resources/lang/vendor/filament/zh_CN/components/modal.php:
--------------------------------------------------------------------------------
1 | [
6 |
7 | 'close' => [
8 | 'label' => '关闭',
9 | ],
10 |
11 | ],
12 |
13 | ];
14 |
--------------------------------------------------------------------------------
/resources/lang/vendor/filament/zh_TW/components/modal.php:
--------------------------------------------------------------------------------
1 | [
6 |
7 | 'close' => [
8 | 'label' => '關閉',
9 | ],
10 |
11 | ],
12 |
13 | ];
14 |
--------------------------------------------------------------------------------
/.mcp.json:
--------------------------------------------------------------------------------
1 | {
2 | "mcpServers": {
3 | "laravel-boost": {
4 | "command": "php",
5 | "args": [
6 | "artisan",
7 | "boost:mcp"
8 | ]
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/.cursor/mcp.json:
--------------------------------------------------------------------------------
1 | {
2 | "mcpServers": {
3 | "laravel-boost": {
4 | "command": "php",
5 | "args": [
6 | "artisan",
7 | "boost:mcp"
8 | ]
9 | }
10 | }
11 | }
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
2 |
3 | *.blade.php diff=html
4 | *.css diff=css
5 | *.html diff=html
6 | *.md diff=markdown
7 | *.php diff=php
8 |
9 | /.github export-ignore
10 | CHANGELOG.md export-ignore
11 | .styleci.yml export-ignore
12 |
--------------------------------------------------------------------------------
/bootstrap/providers.php:
--------------------------------------------------------------------------------
1 | '',
10 | 'key' => '',
11 |
12 | // 此地址一般无需更改
13 | 'api_url' => 'https://payjs.cn/api/',
14 | ];
15 |
--------------------------------------------------------------------------------
/public/js/marcelweidum/filament-expiration-notice/filament-expiration-notice-scripts.js:
--------------------------------------------------------------------------------
1 | document.addEventListener("livewire:init",()=>{let e=!1;Livewire.hook("request",({fail:i})=>{i(({status:r,preventDefault:n})=>{r===419&&(n(),!e&&(e=!0,Livewire.dispatch("open-modal",{id:"session-expired"})))})})});
2 |
--------------------------------------------------------------------------------
/boost.json:
--------------------------------------------------------------------------------
1 | {
2 | "agents": [
3 | "claude_code",
4 | "codex",
5 | "cursor",
6 | "phpstorm"
7 | ],
8 | "editors": [
9 | "claude_code",
10 | "cursor",
11 | "phpstorm",
12 | "vscode"
13 | ],
14 | "guidelines": []
15 | }
16 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | 暂无图片
4 |
5 |
--------------------------------------------------------------------------------
/routes/web.php:
--------------------------------------------------------------------------------
1 | comment(Inspiring::quote());
12 | })->purpose('Display an inspiring quote');
13 |
--------------------------------------------------------------------------------
/phpstan.neon.dist:
--------------------------------------------------------------------------------
1 | includes:
2 | - vendor/larastan/larastan/extension.neon
3 | - vendor/nesbot/carbon/extension.neon
4 |
5 | parameters:
6 |
7 | paths:
8 | - app/
9 |
10 | # Level 10 is the highest level
11 | level: 5
12 |
13 | # ignoreErrors:
14 | # - '#PHPDoc tag @var#'
15 | #
16 | # excludePaths:
17 | # - ./*/*/FileToBeExcluded.php
18 |
--------------------------------------------------------------------------------
/config/dujiaoka.php:
--------------------------------------------------------------------------------
1 | 'next-1.0.0',
10 | // 语言
11 | 'language' => [
12 | 'zh_CN' => '简体中文',
13 | 'zh_TW' => '繁体中文',
14 | ],
15 | // 后台管理路径
16 | 'admin_path' => env('ADMIN_PATH', 'admin'),
17 | ];
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.log
2 | .DS_Store
3 | .env
4 | .env.backup
5 | .env.production
6 | .phpactor.json
7 | .phpunit.result.cache
8 | /.fleet
9 | /.idea
10 | /.nova
11 | /.phpunit.cache
12 | /.vscode
13 | /.zed
14 | /auth.json
15 | /node_modules
16 | # /public/build
17 | /public/hot
18 | /public/storage
19 | /storage/*.key
20 | /storage/pail
21 | /vendor
22 | Homestead.json
23 | Homestead.yaml
24 | Thumbs.db
25 |
--------------------------------------------------------------------------------
/app/Exceptions/AppException.php:
--------------------------------------------------------------------------------
1 | [
10 | 'Emailtpl' => '邮件模板',
11 | 'emailtpl' => '邮件模板',
12 | ],
13 | 'fields' => [
14 | 'tpl_name' => '邮件标题',
15 | 'tpl_content' => '邮件内容',
16 | 'tpl_token' => '邮件标识',
17 | ],
18 | 'options' => [
19 | ],
20 | ];
21 |
--------------------------------------------------------------------------------
/resources/lang/zh_CN/goods-group.php:
--------------------------------------------------------------------------------
1 | [
10 | 'GoodsGroup' => '分类',
11 | 'goods-group' => '分类',
12 | ],
13 | 'fields' => [
14 | 'gp_name' => '分类名称',
15 | 'is_open' => '是否启用',
16 | 'ord' => '排序权重 越大越靠前',
17 | ],
18 | 'options' => [
19 | ],
20 | ];
21 |
--------------------------------------------------------------------------------
/resources/lang/zh_TW/emailtpl.php:
--------------------------------------------------------------------------------
1 | [
10 | 'Emailtpl' => '郵件模板',
11 | 'emailtpl' => '郵件模板',
12 | ],
13 | 'fields' => [
14 | 'tpl_name' => '郵件標題',
15 | 'tpl_content' => '郵件內容',
16 | 'tpl_token' => '郵件標識',
17 | ],
18 | 'options' => [
19 | ],
20 | ];
21 |
--------------------------------------------------------------------------------
/resources/lang/zh_TW/goods-group.php:
--------------------------------------------------------------------------------
1 | [
10 | 'GoodsGroup' => '分類',
11 | 'goods-group' => '分類',
12 | ],
13 | 'fields' => [
14 | 'gp_name' => '分類名稱',
15 | 'is_open' => '是否啟用',
16 | 'ord' => '排序權重 越大越靠前',
17 | ],
18 | 'options' => [
19 | ],
20 | ];
21 |
--------------------------------------------------------------------------------
/app/Filament/Resources/Carmis/Pages/CreateCarmis.php:
--------------------------------------------------------------------------------
1 | [
10 | 'success' => '发送成功',
11 | 'to' => '收件人',
12 | 'title' => '邮件标题',
13 | 'body' => '邮件内容',
14 | ],
15 |
16 | 'fields' => [
17 |
18 | ],
19 | 'options' => [
20 | ],
21 | 'rule_messages' => [
22 | ],
23 | ];
24 |
--------------------------------------------------------------------------------
/app/Filament/Resources/Coupons/Pages/CreateCoupon.php:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/app/Filament/Resources/GoodsGroups/Pages/CreateGoodsGroup.php:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/resources/views/filament/pages/system-setting.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://json.schemastore.org/package.json",
3 | "private": true,
4 | "type": "module",
5 | "scripts": {
6 | "build": "vite build",
7 | "dev": "vite"
8 | },
9 | "devDependencies": {
10 | "@tailwindcss/vite": "^4.1.16",
11 | "axios": "^1.11.0",
12 | "concurrently": "^9.0.1",
13 | "laravel-vite-plugin": "^2.0.0",
14 | "tailwindcss": "^4.1.16",
15 | "vite": "^7.0.7"
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/artisan:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | handleCommand(new ArgvInput);
17 |
18 | exit($status);
19 |
--------------------------------------------------------------------------------
/app/Http/Middleware/PayGateWay.php:
--------------------------------------------------------------------------------
1 | handleRequest(Request::capture());
21 |
--------------------------------------------------------------------------------
/public/js/filament/forms/components/textarea.js:
--------------------------------------------------------------------------------
1 | function r({initialHeight:t,shouldAutosize:i,state:s}){return{state:s,wrapperEl:null,init(){this.wrapperEl=this.$el.parentNode,this.setInitialHeight(),i?this.$watch("state",()=>{this.resize()}):this.setUpResizeObserver()},setInitialHeight(){this.$el.scrollHeight<=0||(this.wrapperEl.style.height=t+"rem")},resize(){if(this.setInitialHeight(),this.$el.scrollHeight<=0)return;let e=this.$el.scrollHeight+"px";this.wrapperEl.style.height!==e&&(this.wrapperEl.style.height=e)},setUpResizeObserver(){new ResizeObserver(()=>{this.wrapperEl.style.height=this.$el.style.height}).observe(this.$el)}}}export{r as default};
2 |
--------------------------------------------------------------------------------
/app/Filament/Resources/Carmis/Pages/ListCarmis.php:
--------------------------------------------------------------------------------
1 | withPaths([
12 | __DIR__.'/app',
13 | __DIR__.'/bootstrap',
14 | __DIR__.'/config',
15 | __DIR__.'/public',
16 | __DIR__.'/resources',
17 | __DIR__.'/routes',
18 | __DIR__.'/tests',
19 | ])
20 | ->withTypeCoverageLevel(0)
21 | ->withDeadCodeLevel(0)
22 | ->withCodeQualityLevel(0)
23 | ->withSetProviders(LaravelSetProvider::class)
24 | ->withComposerBased(laravel: true);
25 |
--------------------------------------------------------------------------------
/app/Service/EmailtplService.php:
--------------------------------------------------------------------------------
1 |
20 | * @copyright assimon
21 | *
22 | * @link http://utf8.hk/
23 | */
24 | public function detailByToken(string $token): Emailtpl
25 | {
26 | $tpl = Emailtpl::query()->where('tpl_token', $token)->first();
27 |
28 | return $tpl;
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/resources/lang/zh_CN/coupon.php:
--------------------------------------------------------------------------------
1 | [
10 | 'Coupon' => '优惠码',
11 | 'coupon' => '优惠码',
12 | ],
13 | 'fields' => [
14 | 'type' => '优惠类型',
15 | 'discount' => '优惠金额',
16 | 'is_use' => '是否已经使用',
17 | 'is_open' => '是否启用',
18 | 'coupon' => '优惠码',
19 | 'ret' => '剩余使用次数',
20 | 'type_one_time' => '一次性使用',
21 | 'type_repeat' => '重复使用',
22 | 'status_use' => '已使用',
23 | 'status_unused' => '未使用',
24 | 'goods_id' => '可用商品',
25 | ],
26 | 'options' => [
27 | ],
28 | ];
29 |
--------------------------------------------------------------------------------
/resources/lang/zh_TW/coupon.php:
--------------------------------------------------------------------------------
1 | [
10 | 'Coupon' => '折扣碼',
11 | 'coupon' => '折扣碼',
12 | ],
13 | 'fields' => [
14 | 'type' => '折扣類型',
15 | 'discount' => '折扣金額',
16 | 'is_use' => '是否已經使用',
17 | 'is_open' => '是否啟用',
18 | 'coupon' => '折扣碼',
19 | 'ret' => '剩余使用次數',
20 | 'type_one_time' => '一次性使用',
21 | 'type_repeat' => '重復使用',
22 | 'status_use' => '已使用',
23 | 'status_unused' => '未使用',
24 | 'goods_id' => '可用商品',
25 | ],
26 | 'options' => [
27 | ],
28 | ];
29 |
--------------------------------------------------------------------------------
/resources/lang/en/pagination.php:
--------------------------------------------------------------------------------
1 | '« Previous',
22 | 'next' => 'Next »',
23 |
24 | ];
25 |
--------------------------------------------------------------------------------
/database/seeders/DatabaseSeeder.php:
--------------------------------------------------------------------------------
1 | call([
24 | EmailTemplateSeeder::class,
25 | PaySeeder::class,
26 | SystemSettingSeeder::class,
27 | ]);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/resources/lang/vendor/filament/zh_CN/components/pagination.php:
--------------------------------------------------------------------------------
1 | '分页',
6 |
7 | 'overview' => '{1} 只有 1 条记录|[2,*] 当前显示第 :first 条到第 :last 条,共 :total 条',
8 |
9 | 'fields' => [
10 |
11 | 'records_per_page' => [
12 |
13 | 'label' => '每页',
14 |
15 | 'options' => [
16 | 'all' => '所有',
17 | ],
18 |
19 | ],
20 |
21 | ],
22 |
23 | 'actions' => [
24 |
25 | 'go_to_page' => [
26 | 'label' => '跳转到 :page',
27 | ],
28 |
29 | 'next' => [
30 | 'label' => '下一页',
31 | ],
32 |
33 | 'previous' => [
34 | 'label' => '上一页',
35 | ],
36 |
37 | ],
38 |
39 | ];
40 |
--------------------------------------------------------------------------------
/app/Listeners/GoodsGroupDeleted.php:
--------------------------------------------------------------------------------
1 | where('group_id', $event->goodsGroup->id)->delete();
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/app/Listeners/GoodsDeleted.php:
--------------------------------------------------------------------------------
1 | where('goods_id', $event->goods->id)->delete();
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/Filament/Resources/Pays/Pages/EditPay.php:
--------------------------------------------------------------------------------
1 | 'These credentials do not match our records.',
22 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
23 |
24 | ];
25 |
--------------------------------------------------------------------------------
/app/Filament/Resources/Emailtpls/Pages/EditEmailtpl.php:
--------------------------------------------------------------------------------
1 | e!==t)},reorderTags(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{"x-on:blur":"createTag()","x-model":"newTag","x-on:keydown"(t){["Enter",...a].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},"x-on:paste"(){this.$nextTick(()=>{if(a.length===0){this.createTag();return}let t=a.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{s as default};
2 |
--------------------------------------------------------------------------------
/public/js/filament/schemas/components/tabs.js:
--------------------------------------------------------------------------------
1 | function u({activeTab:a,isTabPersistedInQueryString:e,livewireId:h,tab:o,tabQueryStringKey:s}){return{tab:o,init(){let t=this.getTabs(),i=new URLSearchParams(window.location.search);e&&i.has(s)&&t.includes(i.get(s))&&(this.tab=i.get(s)),this.$watch("tab",()=>this.updateQueryString()),(!this.tab||!t.includes(this.tab))&&(this.tab=t[a-1]),Livewire.hook("commit",({component:r,commit:f,succeed:c,fail:l,respond:b})=>{c(({snapshot:d,effect:m})=>{this.$nextTick(()=>{if(r.id!==h)return;let n=this.getTabs();n.includes(this.tab)||(this.tab=n[a-1]??this.tab)})})})},getTabs(){return this.$refs.tabsData?JSON.parse(this.$refs.tabsData.value):[]},updateQueryString(){if(!e)return;let t=new URL(window.location.href);t.searchParams.set(s,this.tab),history.replaceState(null,document.title,t.toString())}}}export{u as default};
2 |
--------------------------------------------------------------------------------
/app/Filament/Resources/GoodsGroups/Pages/EditGoodsGroup.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | Options -MultiViews -Indexes
4 |
5 |
6 | RewriteEngine On
7 |
8 | # Handle Authorization Header
9 | RewriteCond %{HTTP:Authorization} .
10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
11 |
12 | # Handle X-XSRF-Token Header
13 | RewriteCond %{HTTP:x-xsrf-token} .
14 | RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
15 |
16 | # Redirect Trailing Slashes If Not A Folder...
17 | RewriteCond %{REQUEST_FILENAME} !-d
18 | RewriteCond %{REQUEST_URI} (.+)/$
19 | RewriteRule ^ %1 [L,R=301]
20 |
21 | # Send Requests To Front Controller...
22 | RewriteCond %{REQUEST_FILENAME} !-d
23 | RewriteCond %{REQUEST_FILENAME} !-f
24 | RewriteRule ^ index.php [L]
25 |
26 |
--------------------------------------------------------------------------------
/resources/lang/zh_CN/pay.php:
--------------------------------------------------------------------------------
1 | [
10 | 'Pay' => '支付通道',
11 | 'pay' => '支付通道',
12 | ],
13 | 'fields' => [
14 | 'merchant_id' => '商户 ID',
15 | 'merchant_key' => '商户 KEY',
16 | 'merchant_pem' => '商户密钥',
17 | 'pay_check' => '支付标识',
18 | 'pay_client' => '支付场景',
19 | 'pay_handleroute' => '支付处理路由',
20 | 'pay_method' => '支付方式',
21 | 'pay_name' => '支付名称',
22 | 'is_open' => '是否启用',
23 | 'method_jump' => '跳转',
24 | 'method_scan' => '扫码',
25 | 'pay_client_pc' => '电脑PC',
26 | 'pay_client_mobile' => '手机',
27 | 'pay_client_all' => '通用',
28 | ],
29 | 'options' => [
30 | ],
31 | ];
32 |
--------------------------------------------------------------------------------
/resources/lang/zh_TW/pay.php:
--------------------------------------------------------------------------------
1 | [
10 | 'Pay' => '支付通道',
11 | 'pay' => '支付通道',
12 | ],
13 | 'fields' => [
14 | 'merchant_id' => '商戶 ID',
15 | 'merchant_key' => '商戶 KEY',
16 | 'merchant_pem' => '商戶金鑰',
17 | 'pay_check' => '支付標識',
18 | 'pay_client' => '支付場景',
19 | 'pay_handleroute' => '支付處理路由',
20 | 'pay_method' => '支付方式',
21 | 'pay_name' => '支付名稱',
22 | 'is_open' => '是否啟用',
23 | 'method_jump' => '跳躍',
24 | 'method_scan' => '掃碼',
25 | 'pay_client_pc' => '計算機PC',
26 | 'pay_client_mobile' => '行動電話',
27 | 'pay_client_all' => '通用',
28 | ],
29 | 'options' => [
30 | ],
31 | ];
32 |
--------------------------------------------------------------------------------
/public/js/filament/tables/components/columns/checkbox.js:
--------------------------------------------------------------------------------
1 | function o({name:i,recordKey:s,state:a}){return{error:void 0,isLoading:!1,state:a,init(){Livewire.hook("commit",({component:e,commit:r,succeed:n,fail:h,respond:u})=>{n(({snapshot:f,effect:d})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||Alpine.raw(this.state)===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||Alpine.raw(this.state)===e)return;this.isLoading=!0;let r=await this.$wire.updateTableColumnState(i,s,this.state);this.error=r?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.state?"1":"0"),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[1,"1"].includes(this.$refs.serverState.value)}}}export{o as default};
2 |
--------------------------------------------------------------------------------
/public/js/filament/tables/components/columns/toggle.js:
--------------------------------------------------------------------------------
1 | function o({name:i,recordKey:s,state:a}){return{error:void 0,isLoading:!1,state:a,init(){Livewire.hook("commit",({component:e,commit:r,succeed:n,fail:h,respond:u})=>{n(({snapshot:f,effect:d})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||Alpine.raw(this.state)===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||Alpine.raw(this.state)===e)return;this.isLoading=!0;let r=await this.$wire.updateTableColumnState(i,s,this.state);this.error=r?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.state?"1":"0"),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[1,"1"].includes(this.$refs.serverState.value)}}}export{o as default};
2 |
--------------------------------------------------------------------------------
/resources/css/filament/admin/theme.css:
--------------------------------------------------------------------------------
1 | @import '../../../../vendor/filament/filament/resources/css/theme.css';
2 |
3 | @source '../../../../app/Filament/**/*';
4 | @source '../../../../resources/views/filament/**/*';
5 |
6 | :root {
7 | text-autospace: normal;
8 | }
9 |
10 | .fi-body {
11 | @apply bg-stone-100 dark:bg-stone-950;
12 | }
13 |
14 | .fi-sidebar {
15 | @apply bg-white dark:bg-gray-900 border-r border-gray-950/5 dark:border-white/10 font-normal;
16 |
17 | & .fi-sidebar-nav-groups {
18 | @apply gap-y-2;
19 | }
20 |
21 | & .fi-sidebar-nav {
22 | @apply px-5 pt-5;
23 | }
24 | }
25 |
26 | .fi-sidebar-nav::-webkit-scrollbar {
27 | display: none;
28 | }
29 |
30 | .fi-sidebar-nav {
31 | -ms-overflow-style: none;
32 | scrollbar-width: none;
33 | }
34 |
35 | .fi-page-header-main-ctn {
36 | @apply gap-y-6 py-6;
37 | }
38 |
--------------------------------------------------------------------------------
/public/js/filament/schemas/components/actions.js:
--------------------------------------------------------------------------------
1 | var i=()=>({isSticky:!1,width:0,resizeObserver:null,boundUpdateWidth:null,init(){let e=this.$el.parentElement;e&&(this.updateWidth(),this.resizeObserver=new ResizeObserver(()=>this.updateWidth()),this.resizeObserver.observe(e),this.boundUpdateWidth=this.updateWidth.bind(this),window.addEventListener("resize",this.boundUpdateWidth))},enableSticky(){this.isSticky=this.$el.getBoundingClientRect().top>0},disableSticky(){this.isSticky=!1},updateWidth(){let e=this.$el.parentElement;if(!e)return;let t=getComputedStyle(this.$root.querySelector(".fi-ac"));this.width=e.offsetWidth+parseInt(t.marginInlineStart,10)*-1+parseInt(t.marginInlineEnd,10)*-1},destroy(){this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.boundUpdateWidth&&(window.removeEventListener("resize",this.boundUpdateWidth),this.boundUpdateWidth=null)}});export{i as default};
2 |
--------------------------------------------------------------------------------
/resources/lang/vendor/filament/zh_TW/components/pagination.php:
--------------------------------------------------------------------------------
1 | '分頁導航',
6 |
7 | 'overview' => '正在顯示第 :first 至 :last 項結果,共 :total 項',
8 |
9 | 'fields' => [
10 |
11 | 'records_per_page' => [
12 |
13 | 'label' => '每頁顯示',
14 |
15 | 'options' => [
16 | 'all' => '全部',
17 | ],
18 |
19 | ],
20 |
21 | ],
22 |
23 | 'actions' => [
24 |
25 | 'first' => [
26 | 'label' => '第一頁',
27 | ],
28 |
29 | 'go_to_page' => [
30 | 'label' => '前往第 :page 頁',
31 | ],
32 |
33 | 'last' => [
34 | 'label' => '最後一頁',
35 | ],
36 |
37 | 'next' => [
38 | 'label' => '下一頁',
39 | ],
40 |
41 | 'previous' => [
42 | 'label' => '上一頁',
43 | ],
44 |
45 | ],
46 |
47 | ];
48 |
--------------------------------------------------------------------------------
/database/migrations/2025_11_01_013553_create_coupons_goods_table.php:
--------------------------------------------------------------------------------
1 | id();
20 | $table->unsignedInteger('goods_id')->comment('商品id');
21 | $table->unsignedInteger('coupons_id')->comment('优惠码id');
22 | });
23 | }
24 |
25 | /**
26 | * Reverse the migrations.
27 | */
28 | public function down(): void
29 | {
30 | Schema::dropIfExists('coupons_goods');
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/app/Http/Middleware/DujiaoSystem.php:
--------------------------------------------------------------------------------
1 | getScheme() == 'https') {
25 | $httpsConfig = [
26 | 'https' => true,
27 | ];
28 | config([
29 | 'admin' => array_merge(config('admin'), $httpsConfig),
30 | ]);
31 | (new AppServiceProvider(app()))->register();
32 | }
33 |
34 | return $next($request);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/app/Rules/SearchPwd.php:
--------------------------------------------------------------------------------
1 | 'Your password has been reset!',
22 | 'sent' => 'We have e-mailed your password reset link!',
23 | 'throttled' => 'Please wait before retrying.',
24 | 'token' => 'This password reset token is invalid.',
25 | 'user' => "We can't find a user with that e-mail address.",
26 |
27 | ];
28 |
--------------------------------------------------------------------------------
/resources/lang/vendor/filament/en/components/pagination.php:
--------------------------------------------------------------------------------
1 | 'Pagination navigation',
6 |
7 | 'overview' => '{1} Showing 1 result|[2,*] Showing :first to :last of :total results',
8 |
9 | 'fields' => [
10 |
11 | 'records_per_page' => [
12 |
13 | 'label' => 'Per page',
14 |
15 | 'options' => [
16 | 'all' => 'All',
17 | ],
18 |
19 | ],
20 |
21 | ],
22 |
23 | 'actions' => [
24 |
25 | 'first' => [
26 | 'label' => 'First',
27 | ],
28 |
29 | 'go_to_page' => [
30 | 'label' => 'Go to page :page',
31 | ],
32 |
33 | 'last' => [
34 | 'label' => 'Last',
35 | ],
36 |
37 | 'next' => [
38 | 'label' => 'Next',
39 | ],
40 |
41 | 'previous' => [
42 | 'label' => 'Previous',
43 | ],
44 |
45 | ],
46 |
47 | ];
48 |
--------------------------------------------------------------------------------
/public/js/filament/forms/components/key-value.js:
--------------------------------------------------------------------------------
1 | function h({state:r}){return{state:r,rows:[],init(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(e,t)=>{let s=i=>i===null?0:Array.isArray(i)?i.length:typeof i!="object"?0:Object.keys(i).length;s(e)===0&&s(t)===0||this.updateRows()})},addRow(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow(e){this.rows.splice(e,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows(e){let t=Alpine.raw(this.rows);this.rows=[];let s=t.splice(e.oldIndex,1)[0];t.splice(e.newIndex,0,s),this.$nextTick(()=>{this.rows=t,this.updateState()})},updateRows(){let t=Alpine.raw(this.state).map(({key:s,value:i})=>({key:s,value:i}));this.rows.forEach(s=>{(s.key===""||s.key===null)&&t.push({key:"",value:s.value})}),this.rows=t},updateState(){let e=[];this.rows.forEach(t=>{t.key===""||t.key===null||e.push({key:t.key,value:t.value})}),JSON.stringify(this.state)!==JSON.stringify(e)&&(this.state=e)}}}export{h as default};
2 |
--------------------------------------------------------------------------------
/database/migrations/2025_11_02_120443_create_notifications_table.php:
--------------------------------------------------------------------------------
1 | uuid('id')->primary();
20 | $table->string('type');
21 | $table->morphs('notifiable');
22 | $table->text('data');
23 | $table->timestamp('read_at')->nullable();
24 | $table->timestamps();
25 | });
26 | }
27 |
28 | /**
29 | * Reverse the migrations.
30 | */
31 | public function down(): void
32 | {
33 | Schema::dropIfExists('notifications');
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/app/Events/GoodsDeleted.php:
--------------------------------------------------------------------------------
1 | goods = $goods;
30 | }
31 |
32 | /**
33 | * Get the channels the event should broadcast on.
34 | *
35 | * @return \Illuminate\Broadcasting\Channel|array
36 | */
37 | public function broadcastOn()
38 | {
39 | return new PrivateChannel('channel-name');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/app/Events/OrderUpdated.php:
--------------------------------------------------------------------------------
1 | order = $order;
30 | }
31 |
32 | /**
33 | * Get the channels the event should broadcast on.
34 | *
35 | * @return \Illuminate\Broadcasting\Channel|array
36 | */
37 | public function broadcastOn()
38 | {
39 | return new PrivateChannel('channel-name');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/database/migrations/2025_11_02_120452_create_failed_import_rows_table.php:
--------------------------------------------------------------------------------
1 | id();
20 | $table->json('data');
21 | $table->foreignId('import_id')->constrained()->cascadeOnDelete();
22 | $table->text('validation_error')->nullable();
23 | $table->timestamps();
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | */
30 | public function down(): void
31 | {
32 | Schema::dropIfExists('failed_import_rows');
33 | }
34 | };
35 |
--------------------------------------------------------------------------------
/public/js/filament/actions/actions.js:
--------------------------------------------------------------------------------
1 | (()=>{var n=({livewireId:e})=>({actionNestingIndex:null,init(){window.addEventListener("sync-action-modals",t=>{t.detail.id===e&&this.syncActionModals(t.detail.newActionNestingIndex)})},syncActionModals(t){if(this.actionNestingIndex===t){this.actionNestingIndex!==null&&this.$nextTick(()=>this.openModal());return}if(this.actionNestingIndex!==null&&this.closeModal(),this.actionNestingIndex=t,this.actionNestingIndex!==null){if(!this.$el.querySelector(`#${this.generateModalId(t)}`)){this.$nextTick(()=>this.openModal());return}this.openModal()}},generateModalId(t){return`fi-${e}-action-`+t},openModal(){let t=this.generateModalId(this.actionNestingIndex);document.dispatchEvent(new CustomEvent("open-modal",{bubbles:!0,composed:!0,detail:{id:t}}))},closeModal(){let t=this.generateModalId(this.actionNestingIndex);document.dispatchEvent(new CustomEvent("close-modal-quietly",{bubbles:!0,composed:!0,detail:{id:t}}))}});document.addEventListener("alpine:init",()=>{window.Alpine.data("filamentActionModals",n)});})();
2 |
--------------------------------------------------------------------------------
/public/js/filament/tables/components/columns/text-input.js:
--------------------------------------------------------------------------------
1 | function o({name:i,recordKey:s,state:a}){return{error:void 0,isLoading:!1,state:a,init(){Livewire.hook("commit",({component:e,commit:r,succeed:n,fail:d,respond:u})=>{n(({snapshot:f,effect:h})=>{this.$nextTick(()=>{if(this.isLoading||e.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let t=this.getServerState();t===void 0||this.getNormalizedState()===t||(this.state=t)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||this.getNormalizedState()===e)return;this.isLoading=!0;let r=await this.$wire.updateTableColumnState(i,s,this.state);this.error=r?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.getNormalizedState()),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[null,void 0].includes(this.$refs.serverState.value)?"":this.$refs.serverState.value.replaceAll('\\"','"')},getNormalizedState(){let e=Alpine.raw(this.state);return[null,void 0].includes(e)?"":e}}}export{o as default};
2 |
--------------------------------------------------------------------------------
/app/Events/GoodsGroupDeleted.php:
--------------------------------------------------------------------------------
1 | goodsGroup = $goodsGroup;
30 | }
31 |
32 | /**
33 | * Get the channels the event should broadcast on.
34 | *
35 | * @return \Illuminate\Broadcasting\Channel|array
36 | */
37 | public function broadcastOn()
38 | {
39 | return new PrivateChannel('channel-name');
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/resources/lang/zh_CN/carmis.php:
--------------------------------------------------------------------------------
1 | [
10 | 'Carmis' => '卡密',
11 | 'carmis' => '卡密',
12 | ],
13 | 'fields' => [
14 | 'goods_id' => '所属商品',
15 | 'status' => '状态',
16 | 'carmi' => '卡密内容',
17 | 'status_unsold' => '未售出',
18 | 'status_sold' => '已售出',
19 | 'is_loop' => '循环卡密',
20 | 'yes' => '是',
21 | 'import_carmis' => '导入卡密',
22 | 'carmis_list' => '卡密列表',
23 | 'carmis_txt' => '卡密文本',
24 | 'are_you_import_sure' => '确定要导入卡密吗?',
25 | 'remove_duplication' => '是否去重',
26 | ],
27 | 'options' => [
28 | ],
29 | 'helps' => [
30 | 'carmis_list' => '一行一个,回车分隔。请勿导入单个文本长度过大的卡密,容易导致内存溢出。如果卡密过大建议修改商品为人工处理',
31 | ],
32 | 'rule_messages' => [
33 | 'carmis_list_and_carmis_txt_can_not_be_empty' => '请填写需要导入的卡密或选择需要上传的卡密文件',
34 | 'import_carmis_success' => '导入卡密成功!',
35 | ],
36 | ];
37 |
--------------------------------------------------------------------------------
/resources/lang/zh_TW/carmis.php:
--------------------------------------------------------------------------------
1 | [
10 | 'Carmis' => '卡密',
11 | 'carmis' => '卡密',
12 | ],
13 | 'fields' => [
14 | 'goods_id' => '所屬商品',
15 | 'status' => '狀態',
16 | 'carmi' => '卡密內容',
17 | 'status_unsold' => '未售出',
18 | 'status_sold' => '已售出',
19 | 'is_loop' => '循環卡密',
20 | 'yes' => '是',
21 | 'import_carmis' => '匯入卡密',
22 | 'carmis_list' => '卡密清單',
23 | 'carmis_txt' => '卡密文字',
24 | 'are_you_import_sure' => '確定要匯入卡密嗎?',
25 | 'remove_duplication' => '是否去重',
26 | ],
27 | 'options' => [
28 | ],
29 | 'helps' => [
30 | 'carmis_list' => '一行一個,輸入鍵分隔。請勿匯入單個文字長度過大的卡密,容易導致記憶體溢出。如果卡密過大建議修改商品為人工處理',
31 | ],
32 | 'rule_messages' => [
33 | 'carmis_list_and_carmis_txt_can_not_be_empty' => '請填寫需要匯入的卡密或選取需要上傳的卡密檔案',
34 | 'import_carmis_success' => '匯入卡密成功!',
35 | ],
36 | ];
37 |
--------------------------------------------------------------------------------
/database/migrations/2025_11_01_013547_create_goods_group_table.php:
--------------------------------------------------------------------------------
1 | id();
20 | $table->string('gp_name', 200)->comment('分类名称');
21 | $table->boolean('is_open')->default(true)->comment('是否启用,1是 0否');
22 | $table->integer('ord')->default(1)->comment('排序权重 越大越靠前');
23 | $table->timestamps();
24 | $table->softDeletes();
25 | });
26 | }
27 |
28 | /**
29 | * Reverse the migrations.
30 | */
31 | public function down(): void
32 | {
33 | Schema::dropIfExists('goods_group');
34 | }
35 | };
36 |
--------------------------------------------------------------------------------
/app/Models/GoodsGroup.php:
--------------------------------------------------------------------------------
1 | GoodsGroupDeleted::class,
28 | ];
29 |
30 | /**
31 | * 关联商品
32 | *
33 | * @return \Illuminate\Database\Eloquent\Relations\HasMany
34 | *
35 | * @author assimon
36 | * @copyright assimon
37 | *
38 | * @link http://utf8.hk/
39 | */
40 | public function goods()
41 | {
42 | return $this->hasMany(Goods::class, 'group_id');
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/database/factories/GoodsGroupFactory.php:
--------------------------------------------------------------------------------
1 |
14 | */
15 | class GoodsGroupFactory extends Factory
16 | {
17 | /**
18 | * Define the model's default state.
19 | *
20 | * @return array
21 | */
22 | public function definition(): array
23 | {
24 | static $order = 100;
25 |
26 | $categories = [
27 | '游戏账号', '游戏道具', '游戏点卡', '游戏代练',
28 | '软件激活码', '会员卡密', '视频会员', '音乐会员',
29 | '云服务器', '域名注册', 'CDN服务', '对象存储',
30 | '在线课程', '电子书', '教程资源', '设计素材',
31 | ];
32 |
33 | return [
34 | 'gp_name' => fake()->unique()->randomElement($categories),
35 | 'is_open' => fake()->boolean(80), // 80% 启用
36 | 'ord' => $order--,
37 | ];
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/resources/lang/zh_TW/global.php:
--------------------------------------------------------------------------------
1 | [
10 | 'id' => 'ID',
11 | 'name' => '名稱',
12 | 'username' => '賬戶',
13 | 'email' => '信箱',
14 | 'http_path' => 'HTTP路徑',
15 | 'password' => '密碼',
16 | 'password_confirmation' => '確認密碼',
17 | 'created_at' => '建立時間',
18 | 'updated_at' => '更新時間',
19 | 'permissions' => '權限',
20 | 'slug' => '標識',
21 | 'user' => '用戶',
22 | 'order' => '排序',
23 | 'ip' => 'IP',
24 | 'method' => '方法',
25 | 'uri' => 'URI',
26 | 'roles' => '角色',
27 | 'path' => '路徑',
28 | 'input' => '輸入',
29 | 'type' => '類型',
30 | ],
31 | 'labels' => [
32 | 'list' => '清單',
33 | 'edit' => '編輯',
34 | 'detail' => '詳細',
35 | 'create' => '建立',
36 | 'root' => '根',
37 | 'scaffold' => '代碼生成器',
38 | ],
39 | 'options' => [
40 | //
41 | ],
42 | ];
43 |
--------------------------------------------------------------------------------
/resources/lang/zh_CN/global.php:
--------------------------------------------------------------------------------
1 | [
10 | 'id' => 'ID',
11 | 'name' => '名称',
12 | 'username' => '用户名',
13 | 'email' => '邮箱',
14 | 'http_path' => 'HTTP路径',
15 | 'password' => '密码',
16 | 'password_confirmation' => '确认密码',
17 | 'created_at' => '创建时间',
18 | 'updated_at' => '更新时间',
19 | 'permissions' => '权限',
20 | 'slug' => '标识',
21 | 'user' => '用户',
22 | 'order' => '排序',
23 | 'ip' => 'IP',
24 | 'method' => '方法',
25 | 'uri' => 'URI',
26 | 'roles' => '角色',
27 | 'path' => '路径',
28 | 'input' => '输入',
29 | 'type' => '类型',
30 | ],
31 | 'labels' => [
32 | 'list' => '列表',
33 | 'edit' => '编辑',
34 | 'detail' => '详细',
35 | 'create' => '创建',
36 | 'root' => '顶级',
37 | 'scaffold' => '代码生成器',
38 | ],
39 | 'options' => [
40 | //
41 | ],
42 | ];
43 |
--------------------------------------------------------------------------------
/database/migrations/2025_11_01_013554_create_emailtpls_table.php:
--------------------------------------------------------------------------------
1 | id();
20 | $table->string('tpl_name', 150)->comment('邮件标题');
21 | $table->text('tpl_content')->comment('邮件内容');
22 | $table->string('tpl_token', 50)->comment('邮件标识');
23 | $table->timestamps();
24 | $table->softDeletes();
25 |
26 | $table->unique('tpl_token', 'mail_token');
27 | });
28 | }
29 |
30 | /**
31 | * Reverse the migrations.
32 | */
33 | public function down(): void
34 | {
35 | Schema::dropIfExists('emailtpls');
36 | }
37 | };
38 |
--------------------------------------------------------------------------------
/database/seeders/SystemSettingSeeder.php:
--------------------------------------------------------------------------------
1 | command->info('System settings already exist in cache. Skipping...');
23 |
24 | return;
25 | }
26 |
27 | // 从配置文件加载默认系统配置
28 | $defaultSettings = config('dujiaoka_settings');
29 |
30 | // 存储到缓存
31 | Cache::forever('system-setting', $defaultSettings);
32 |
33 | $this->command->info('System settings initialized successfully!');
34 | $this->command->info('Default language: '.$defaultSettings['language']);
35 | $this->command->info('Order expire time: '.$defaultSettings['order_expire_time'].' minutes');
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Home/HomeController.php:
--------------------------------------------------------------------------------
1 |
22 | * @copyright assimon
23 | *
24 | * @link http://utf8.hk/
25 | */
26 | public function geetest(Request $request)
27 | {
28 | $data = [
29 | 'user_id' => @Auth::user() ? @Auth::user()->id : 'UnLoginUser',
30 | 'client_type' => 'web',
31 | 'ip_address' => \Illuminate\Support\Facades\Request::ip(),
32 | ];
33 | $status = Geetest::preProcess($data);
34 | session()->put('gtserver', $status);
35 | session()->put('user_id', $data['user_id']);
36 |
37 | return Geetest::getResponseStr();
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/app/Models/BaseModel.php:
--------------------------------------------------------------------------------
1 |
34 | * @copyright assimon
35 | *
36 | * @link http://utf8.hk/
37 | */
38 | public static function getIsOpenMap()
39 | {
40 | return [
41 | self::STATUS_OPEN => admin_trans('dujiaoka.status_open'),
42 | self::STATUS_CLOSE => admin_trans('dujiaoka.status_close'),
43 | ];
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/database/migrations/0001_01_01_000001_create_cache_table.php:
--------------------------------------------------------------------------------
1 | string('key')->primary();
20 | $table->mediumText('value');
21 | $table->integer('expiration');
22 | });
23 |
24 | Schema::create('cache_locks', function (Blueprint $table) {
25 | $table->string('key')->primary();
26 | $table->string('owner');
27 | $table->integer('expiration');
28 | });
29 | }
30 |
31 | /**
32 | * Reverse the migrations.
33 | */
34 | public function down(): void
35 | {
36 | Schema::dropIfExists('cache');
37 | Schema::dropIfExists('cache_locks');
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/docs/development-logs/README.md:
--------------------------------------------------------------------------------
1 | # 开发日志
2 |
3 | 本目录包含独角数卡从 Laravel 6 + dcat-admin 迁移到 Laravel 12 + Filament 4 过程中的开发日志和检查清单。
4 |
5 | ## 文档说明
6 |
7 | 这些文档记录了项目迁移和开发过程中的各个阶段:
8 |
9 | ### 迁移相关
10 | - `MIGRATION_SUMMARY.md` - 迁移总结
11 | - `MIGRATION_VERIFICATION_REPORT.md` - 迁移验证报告
12 | - `LARAVEL12_COMPATIBILITY_REPORT.md` - Laravel 12 兼容性报告
13 | - `FINAL_COMPATIBILITY_CHECK.md` - 最终兼容性检查
14 |
15 | ### 功能开发
16 | - `ADMIN_FEATURE_CHECKLIST.md` - 管理后台功能清单
17 | - `FILAMENT_LOCALIZATION.md` - Filament 本地化实现
18 | - `FILAMENT_LOCALIZATION_SUMMARY.md` - Filament 本地化总结
19 | - `FILAMENT_OPTIMIZATION.md` - Filament 优化记录
20 | - `SEEDING_IMPLEMENTATION_SUMMARY.md` - 数据库种子实现总结
21 |
22 | ### Bug修复
23 | - `COUPON_FIX.md` - 优惠券功能修复记录
24 |
25 | ### 项目管理
26 | - `CHECKLIST_FINAL.md` - 最终检查清单
27 | - `COMPLETION_REPORT.md` - 完成报告
28 | - `TEST_CHECKLIST.md` - 测试清单
29 | - `FILES_SUMMARY.md` - 文件摘要
30 | - `DOCUMENTATION_UPDATES.md` - 文档更新记录
31 | - `UPGRADE_QUICKSTART.md` - 升级快速指南
32 |
33 | ## 注意
34 |
35 | 这些文档主要用于项目开发参考,不作为最终用户文档。最终用户文档请参阅上级目录的以下文件:
36 |
37 | - `README.md` - 项目主文档
38 | - `DEPLOYMENT_GUIDE.md` - 部署指南
39 | - `UPGRADE_GUIDE.md` - 升级指南
40 | - `DATABASE_SEEDING.md` - 数据库种子说明
41 | - `TEST_DATA.md` - 测试数据说明
42 |
--------------------------------------------------------------------------------
/database/migrations/2025_11_01_013550_create_carmis_table.php:
--------------------------------------------------------------------------------
1 | id();
20 | $table->unsignedInteger('goods_id')->comment('所属商品');
21 | $table->tinyInteger('status')->default(1)->comment('状态 1未售出 2已售出');
22 | $table->boolean('is_loop')->default(false)->comment('循环卡密 1是 0否');
23 | $table->text('carmi')->comment('卡密');
24 | $table->timestamps();
25 | $table->softDeletes();
26 |
27 | $table->index('goods_id', 'idx_goods_id');
28 | });
29 | }
30 |
31 | /**
32 | * Reverse the migrations.
33 | */
34 | public function down(): void
35 | {
36 | Schema::dropIfExists('carmis');
37 | }
38 | };
39 |
--------------------------------------------------------------------------------
/app/Filament/Exports/CarmisExporter.php:
--------------------------------------------------------------------------------
1 | label('所属商品'),
25 | ExportColumn::make('carmi')
26 | ->label('卡密内容'),
27 | ExportColumn::make('created_at')
28 | ->label('创建时间'),
29 | ];
30 | }
31 |
32 | public static function getCompletedNotificationBody(Export $export): string
33 | {
34 | $body = '卡密导出已完成,成功导出 '.Number::format($export->successful_rows).' 条记录。';
35 |
36 | if ($failedRowsCount = $export->getFailedRowsCount()) {
37 | $body .= ' 失败 '.Number::format($failedRowsCount).' 条记录。';
38 | }
39 |
40 | return $body;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/database/migrations/2025_11_02_120450_create_imports_table.php:
--------------------------------------------------------------------------------
1 | id();
20 | $table->timestamp('completed_at')->nullable();
21 | $table->string('file_name');
22 | $table->string('file_path');
23 | $table->string('importer');
24 | $table->unsignedInteger('processed_rows')->default(0);
25 | $table->unsignedInteger('total_rows');
26 | $table->unsignedInteger('successful_rows')->default(0);
27 | $table->foreignId('user_id')->constrained()->cascadeOnDelete();
28 | $table->timestamps();
29 | });
30 | }
31 |
32 | /**
33 | * Reverse the migrations.
34 | */
35 | public function down(): void
36 | {
37 | Schema::dropIfExists('imports');
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/database/migrations/2025_11_01_013552_create_coupons_table.php:
--------------------------------------------------------------------------------
1 | id();
20 | $table->decimal('discount', 10, 2)->default(0)->comment('优惠金额');
21 | $table->tinyInteger('is_use')->default(1)->comment('是否已经使用 1未使用 2已使用');
22 | $table->boolean('is_open')->default(true)->comment('是否启用 1是 0否');
23 | $table->string('coupon', 150)->comment('优惠码');
24 | $table->integer('ret')->default(0)->comment('剩余使用次数');
25 | $table->timestamps();
26 | $table->softDeletes();
27 |
28 | $table->unique('coupon', 'idx_coupon');
29 | });
30 | }
31 |
32 | /**
33 | * Reverse the migrations.
34 | */
35 | public function down(): void
36 | {
37 | Schema::dropIfExists('coupons');
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/database/migrations/2025_11_02_120451_create_exports_table.php:
--------------------------------------------------------------------------------
1 | id();
20 | $table->timestamp('completed_at')->nullable();
21 | $table->string('file_disk');
22 | $table->string('file_name')->nullable();
23 | $table->string('exporter');
24 | $table->unsignedInteger('processed_rows')->default(0);
25 | $table->unsignedInteger('total_rows');
26 | $table->unsignedInteger('successful_rows')->default(0);
27 | $table->foreignId('user_id')->constrained()->cascadeOnDelete();
28 | $table->timestamps();
29 | });
30 | }
31 |
32 | /**
33 | * Reverse the migrations.
34 | */
35 | public function down(): void
36 | {
37 | Schema::dropIfExists('exports');
38 | }
39 | };
40 |
--------------------------------------------------------------------------------
/config/services.php:
--------------------------------------------------------------------------------
1 | [
23 | 'token' => env('POSTMARK_TOKEN'),
24 | ],
25 |
26 | 'resend' => [
27 | 'key' => env('RESEND_KEY'),
28 | ],
29 |
30 | 'ses' => [
31 | 'key' => env('AWS_ACCESS_KEY_ID'),
32 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
33 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
34 | ],
35 |
36 | 'slack' => [
37 | 'notifications' => [
38 | 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
39 | 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
40 | ],
41 | ],
42 |
43 | ];
44 |
--------------------------------------------------------------------------------
/config/view.php:
--------------------------------------------------------------------------------
1 | [
22 | resource_path('views'),
23 | ],
24 |
25 | /*
26 | |--------------------------------------------------------------------------
27 | | Compiled View Path
28 | |--------------------------------------------------------------------------
29 | |
30 | | This option determines where all the compiled Blade templates will be
31 | | stored for your application. Typically, this is within the storage
32 | | directory. However, as usual, you are free to change this value.
33 | |
34 | */
35 |
36 | 'compiled' => env(
37 | 'VIEW_COMPILED_PATH',
38 | realpath(storage_path('framework/views'))
39 | ),
40 |
41 | ];
42 |
--------------------------------------------------------------------------------
/app/Models/User.php:
--------------------------------------------------------------------------------
1 | */
18 | use HasFactory, Notifiable;
19 |
20 | /**
21 | * The attributes that are mass assignable.
22 | *
23 | * @var list
24 | */
25 | protected $fillable = [
26 | 'name',
27 | 'email',
28 | 'password',
29 | ];
30 |
31 | /**
32 | * The attributes that should be hidden for serialization.
33 | *
34 | * @var list
35 | */
36 | protected $hidden = [
37 | 'password',
38 | 'remember_token',
39 | ];
40 |
41 | /**
42 | * Get the attributes that should be cast.
43 | *
44 | * @return array
45 | */
46 | protected function casts(): array
47 | {
48 | return [
49 | 'email_verified_at' => 'datetime',
50 | 'password' => 'hashed',
51 | ];
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/resources/lang/zh_TW/order.php:
--------------------------------------------------------------------------------
1 | [
10 | 'Order' => '訂單',
11 | 'order' => '訂單',
12 | ],
13 | 'fields' => [
14 | 'actual_price' => '實際支付價格',
15 | 'buy_amount' => '購買數量',
16 | 'buy_ip' => '購買者下單IP地址',
17 | 'coupon_discount_price' => '折扣碼折扣價格',
18 | 'coupon_id' => '折扣碼',
19 | 'email' => '下單信箱',
20 | 'goods_id' => '所屬商品',
21 | 'goods_price' => '商品單價',
22 | 'info' => '訂單詳情',
23 | 'order_sn' => '訂單號',
24 | 'pay_id' => '支付通道',
25 | 'status' => '訂單狀態',
26 | 'search_pwd' => '查詢密碼',
27 | 'title' => '訂單名稱',
28 | 'total_price' => '訂單總價',
29 | 'trade_no' => '第三方支付訂單號',
30 | 'type' => '訂單類型',
31 | 'wholesale_discount_price' => '批發價折扣',
32 | 'status_wait_pay' => '待支付',
33 | 'status_pending' => '待處理',
34 | 'status_processing' => '處理中',
35 | 'status_completed' => '已完成',
36 | 'status_failure' => '失敗',
37 | 'status_abnormal' => '異常',
38 | 'status_expired' => '已逾期',
39 | 'order_created' => '訂單建立時間',
40 | 'order_detail' => '訂單詳情',
41 | ],
42 | 'options' => [
43 | ],
44 | ];
45 |
--------------------------------------------------------------------------------
/resources/lang/zh_CN/order.php:
--------------------------------------------------------------------------------
1 | [
10 | 'Order' => '订单',
11 | 'order' => '订单',
12 | ],
13 | 'fields' => [
14 | 'actual_price' => '实际支付价格',
15 | 'buy_amount' => '购买数量',
16 | 'buy_ip' => '购买者下单IP地址',
17 | 'coupon_discount_price' => '优惠码优惠价格',
18 | 'coupon_id' => '优惠码',
19 | 'email' => '下单邮箱',
20 | 'goods_id' => '所属商品',
21 | 'goods_price' => '商品单价',
22 | 'info' => '订单详情',
23 | 'order_id' => '订单ID',
24 | 'order_sn' => '订单号',
25 | 'pay_id' => '支付通道',
26 | 'status' => '订单状态',
27 | 'search_pwd' => '查询密码',
28 | 'title' => '订单名称',
29 | 'total_price' => '订单总价',
30 | 'trade_no' => '第三方支付订单号',
31 | 'type' => '订单类型',
32 | 'wholesale_discount_price' => '批发价优惠',
33 | 'status_wait_pay' => '待支付',
34 | 'status_pending' => '待处理',
35 | 'status_processing' => '处理中',
36 | 'status_completed' => '已完成',
37 | 'status_failure' => '失败',
38 | 'status_abnormal' => '异常',
39 | 'status_expired' => '已过期',
40 | 'order_created' => '订单创建时间',
41 | 'order_detail' => '订单详情',
42 | ],
43 | 'options' => [
44 | ],
45 | ];
46 |
--------------------------------------------------------------------------------
/app/Service/CarmisService.php:
--------------------------------------------------------------------------------
1 |
22 | * @copyright assimon
23 | *
24 | * @link http://utf8.hk/
25 | */
26 | public function withGoodsByAmountAndStatusUnsold(int $goodsID, int $byAmount)
27 | {
28 | $carmis = Carmis::query()
29 | ->where('goods_id', $goodsID)
30 | ->where('status', Carmis::STATUS_UNSOLD)
31 | ->take($byAmount)
32 | ->get();
33 |
34 | return $carmis ? $carmis->toArray() : null;
35 | }
36 |
37 | /**
38 | * 通过id集合设置卡密已售出
39 | *
40 | * @param array $ids 卡密id集合
41 | *
42 | * @author assimon
43 | * @copyright assimon
44 | *
45 | * @link http://utf8.hk/
46 | */
47 | public function soldByIDS(array $ids): bool
48 | {
49 | return Carmis::query()->whereIn('id', $ids)->where('is_loop', 0)->update(['status' => Carmis::STATUS_SOLD]);
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/database/factories/UserFactory.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | class UserFactory extends Factory
18 | {
19 | /**
20 | * The current password being used by the factory.
21 | */
22 | protected static ?string $password;
23 |
24 | /**
25 | * Define the model's default state.
26 | *
27 | * @return array
28 | */
29 | public function definition(): array
30 | {
31 | return [
32 | 'name' => fake()->name(),
33 | 'email' => fake()->unique()->safeEmail(),
34 | 'email_verified_at' => now(),
35 | 'password' => static::$password ??= Hash::make('password'),
36 | 'remember_token' => Str::random(10),
37 | ];
38 | }
39 |
40 | /**
41 | * Indicate that the model's email address should be unverified.
42 | */
43 | public function unverified(): static
44 | {
45 | return $this->state(fn (array $attributes) => [
46 | 'email_verified_at' => null,
47 | ]);
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/tests/Pest.php:
--------------------------------------------------------------------------------
1 | extend(Tests\TestCase::class)
8 | // ->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
9 | ->in('Feature');
10 |
11 | /*
12 | |--------------------------------------------------------------------------
13 | | Expectations
14 | |--------------------------------------------------------------------------
15 | |
16 | | When you're writing tests, you often need to check that values meet certain conditions. The
17 | | "expect()" function gives you access to a set of "expectations" methods that you can use
18 | | to assert different things. Of course, you may extend the Expectation API at any time.
19 | |
20 | */
21 |
22 | expect()->extend('toBeOne', function () {
23 | return $this->toBe(1);
24 | });
25 |
26 | /*
27 | |--------------------------------------------------------------------------
28 | | Functions
29 | |--------------------------------------------------------------------------
30 | |
31 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your
32 | | project that you don't want to repeat in every file. Here you can also expose helpers as
33 | | global functions to help you to reduce the number of lines of code in your test files.
34 | |
35 | */
36 |
37 | function something()
38 | {
39 | // ..
40 | }
41 |
--------------------------------------------------------------------------------
/public/js/filament/schemas/components/wizard.js:
--------------------------------------------------------------------------------
1 | function o({isSkippable:s,isStepPersistedInQueryString:i,key:r,startStep:h,stepQueryStringKey:n}){return{step:null,init(){this.$watch("step",()=>this.updateQueryString()),this.step=this.getSteps().at(h-1),this.autofocusFields()},async requestNextStep(){await this.$wire.callSchemaComponentMethod(r,"nextStep",{currentStepIndex:this.getStepIndex(this.step)})},goToNextStep(){let t=this.getStepIndex(this.step)+1;t>=this.getSteps().length||(this.step=this.getSteps()[t],this.autofocusFields(),this.scroll())},goToPreviousStep(){let t=this.getStepIndex(this.step)-1;t<0||(this.step=this.getSteps()[t],this.autofocusFields(),this.scroll())},scroll(){this.$nextTick(()=>{this.$refs.header?.children[this.getStepIndex(this.step)].scrollIntoView({behavior:"smooth",block:"start"})})},autofocusFields(){this.$nextTick(()=>this.$refs[`step-${this.step}`].querySelector("[autofocus]")?.focus())},getStepIndex(t){let e=this.getSteps().findIndex(p=>p===t);return e===-1?0:e},getSteps(){return JSON.parse(this.$refs.stepsData.value)},isFirstStep(){return this.getStepIndex(this.step)<=0},isLastStep(){return this.getStepIndex(this.step)+1>=this.getSteps().length},isStepAccessible(t){return s||this.getStepIndex(this.step)>this.getStepIndex(t)},updateQueryString(){if(!i)return;let t=new URL(window.location.href);t.searchParams.set(n,this.step),history.replaceState(null,document.title,t.toString())}}}export{o as default};
2 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | tests/Unit
10 |
11 |
12 | tests/Feature
13 |
14 |
15 |
16 |
17 | app
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | APP_NAME=Dujiaoka-Next
2 | APP_ENV=local
3 | APP_KEY=
4 | APP_DEBUG=true
5 | APP_URL=http://localhost
6 | APP_TIMEZONE=Asia/Shanghai
7 |
8 | APP_LOCALE=zh_CN
9 | APP_FALLBACK_LOCALE=en
10 | APP_FAKER_LOCALE=en_US
11 |
12 | ADMIN_PATH=admin
13 |
14 | APP_MAINTENANCE_DRIVER=file
15 | # APP_MAINTENANCE_STORE=database
16 |
17 | # PHP_CLI_SERVER_WORKERS=4
18 |
19 | BCRYPT_ROUNDS=12
20 |
21 | LOG_CHANNEL=stack
22 | LOG_STACK=single
23 | LOG_DEPRECATIONS_CHANNEL=null
24 | LOG_LEVEL=debug
25 |
26 | DB_CONNECTION=mysql
27 | DB_HOST=127.0.0.1
28 | DB_PORT=3306
29 | DB_DATABASE=dujiaoka_next
30 | DB_USERNAME=root
31 | DB_PASSWORD=
32 |
33 | SESSION_DRIVER=database
34 | SESSION_LIFETIME=120
35 | SESSION_ENCRYPT=false
36 | SESSION_PATH=/
37 | SESSION_DOMAIN=null
38 |
39 | BROADCAST_CONNECTION=log
40 | FILESYSTEM_DISK=local
41 | QUEUE_CONNECTION=database
42 |
43 | CACHE_STORE=redis
44 | # CACHE_PREFIX=
45 |
46 | MEMCACHED_HOST=127.0.0.1
47 |
48 | REDIS_CLIENT=phpredis
49 | REDIS_HOST=127.0.0.1
50 | REDIS_PASSWORD=null
51 | REDIS_PORT=6379
52 |
53 | MAIL_MAILER=log
54 | MAIL_SCHEME=null
55 | MAIL_HOST=127.0.0.1
56 | MAIL_PORT=2525
57 | MAIL_USERNAME=null
58 | MAIL_PASSWORD=null
59 | MAIL_FROM_ADDRESS="hello@example.com"
60 | MAIL_FROM_NAME="${APP_NAME}"
61 |
62 | AWS_ACCESS_KEY_ID=
63 | AWS_SECRET_ACCESS_KEY=
64 | AWS_DEFAULT_REGION=us-east-1
65 | AWS_BUCKET=
66 | AWS_USE_PATH_STYLE_ENDPOINT=false
67 |
68 | VITE_APP_NAME="${APP_NAME}"
69 |
--------------------------------------------------------------------------------
/app/Models/Coupon.php:
--------------------------------------------------------------------------------
1 |
43 | * @copyright assimon
44 | *
45 | * @link http://utf8.hk/
46 | */
47 | public function goods()
48 | {
49 | return $this->belongsToMany(Goods::class, 'coupons_goods', 'coupons_id', 'goods_id');
50 | }
51 |
52 | public static function getStatusUseMap()
53 | {
54 | return [
55 | self::STATUS_USE => admin_trans('coupon.fields.status_use'),
56 | self::STATUS_UNUSED => admin_trans('coupon.fields.status_unused'),
57 | ];
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/tests/Feature/LivewirePagesTest.php:
--------------------------------------------------------------------------------
1 | get('/');
15 |
16 | $response->assertSuccessful();
17 | $response->assertSeeLivewire('pages.home');
18 | });
19 |
20 | test('search order page renders successfully', function (): void {
21 | $response = $this->get('/search-order');
22 |
23 | $response->assertSuccessful();
24 | $response->assertSeeLivewire('pages.search-order');
25 | });
26 |
27 | test('error page renders successfully', function (): void {
28 | $response = $this->get('/error');
29 |
30 | $response->assertSuccessful();
31 | $response->assertSeeLivewire('pages.error');
32 | });
33 |
34 | test('buy page renders with valid product', function (): void {
35 | $group = GoodsGroup::factory()->create();
36 | $goods = Goods::factory()->create([
37 | 'group_id' => $group->id,
38 | 'in_stock' => 10,
39 | 'is_open' => Goods::STATUS_OPEN,
40 | ]);
41 |
42 | $response = $this->get("/buy/{$goods->id}");
43 |
44 | $response->assertSuccessful();
45 | $response->assertSeeLivewire('pages.buy');
46 | $response->assertSee($goods->gd_name);
47 | });
48 |
--------------------------------------------------------------------------------
/app/Filament/Resources/GoodsGroups/Schemas/GoodsGroupForm.php:
--------------------------------------------------------------------------------
1 | columns(1)
21 | ->components([
22 | Section::make('分类信息')
23 | ->columnSpan('full')
24 | ->schema([
25 | TextInput::make('gp_name')
26 | ->label('分类名称')
27 | ->required()
28 | ->maxLength(200),
29 |
30 | TextInput::make('ord')
31 | ->label('排序权重')
32 | ->numeric()
33 | ->default(1)
34 | ->helperText('数值越大越靠前')
35 | ->required(),
36 |
37 | Toggle::make('is_open')
38 | ->label('是否启用')
39 | ->default(true)
40 | ->inline(false),
41 | ]),
42 | ]);
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/app/Jobs/CouponBack.php:
--------------------------------------------------------------------------------
1 | order = $order;
45 | }
46 |
47 | /**
48 | * Execute the job.
49 | *
50 | * @return void
51 | */
52 | public function handle()
53 | {
54 | // 如果订单有使用优惠码
55 | if ($this->order->coupon_id) {
56 | // 优惠码次数+1
57 | app('Service\CouponService')->retIncrByID($this->order->coupon_id);
58 | // 设置订单优惠码已回退
59 | app('Service\OrderService')->couponIsBack($this->order->order_sn);
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/config/dujiaoka_settings.php:
--------------------------------------------------------------------------------
1 | '🦄 独角数卡 NEXT',
11 | 'img_logo' => null,
12 | 'text_logo' => '🦄 独角数卡 NEXT',
13 | 'keywords' => '独角数卡 NEXT,自动发卡,数字商品',
14 | 'description' => '独角数卡 NEXT - 开源式自动化售货系统',
15 | 'language' => 'zh_CN',
16 | 'manage_email' => null,
17 | 'order_expire_time' => 5,
18 | 'is_open_anti_red' => false,
19 | 'is_open_img_code' => false,
20 | 'is_open_search_pwd' => false,
21 | 'notice' => '欢迎使用独角数卡 NEXT 系统!
',
22 | 'footer' => 'Powered by Dujiaoka NEXT',
23 |
24 | // 订单推送设置
25 | 'is_open_server_jiang' => false,
26 | 'server_jiang_token' => null,
27 | 'is_open_telegram_push' => false,
28 | 'telegram_bot_token' => null,
29 | 'telegram_userid' => null,
30 | 'is_open_bark_push' => false,
31 | 'is_open_bark_push_url' => false,
32 | 'bark_server' => null,
33 | 'bark_token' => null,
34 | 'is_open_qywxbot_push' => false,
35 | 'qywxbot_key' => null,
36 |
37 | // 邮件设置
38 | 'driver' => 'smtp',
39 | 'host' => null,
40 | 'port' => 587,
41 | 'username' => null,
42 | 'password' => null,
43 | 'encryption' => 'tls',
44 | 'from_address' => null,
45 | 'from_name' => '独角发卡',
46 |
47 | // 极验设置
48 | 'geetest_id' => null,
49 | 'geetest_key' => null,
50 | 'is_open_geetest' => false,
51 | ];
52 |
--------------------------------------------------------------------------------
/database/migrations/2025_11_01_013555_create_pays_table.php:
--------------------------------------------------------------------------------
1 | id();
20 | $table->string('pay_name', 200)->comment('支付名称');
21 | $table->string('pay_check', 50)->comment('支付标识');
22 | $table->tinyInteger('pay_method')->comment('支付方式 1跳转 2扫码');
23 | $table->tinyInteger('pay_client')->default(1)->comment('支付场景:1电脑pc 2手机 3全部');
24 | $table->string('merchant_id', 200)->nullable()->comment('商户 ID');
25 | $table->text('merchant_key')->nullable()->comment('商户 KEY');
26 | $table->text('merchant_pem')->comment('商户密钥');
27 | $table->string('pay_handleroute', 200)->comment('支付处理路由');
28 | $table->boolean('is_open')->default(true)->comment('是否启用 1是 0否');
29 | $table->timestamps();
30 | $table->softDeletes();
31 |
32 | $table->unique('pay_check', 'idx_pay_check');
33 | });
34 | }
35 |
36 | /**
37 | * Reverse the migrations.
38 | */
39 | public function down(): void
40 | {
41 | Schema::dropIfExists('pays');
42 | }
43 | };
44 |
--------------------------------------------------------------------------------
/app/Providers/EventServiceProvider.php:
--------------------------------------------------------------------------------
1 | >
23 | */
24 | protected $listen = [
25 | Registered::class => [
26 | SendEmailVerificationNotification::class,
27 | ],
28 | GoodsGroupDeleted::class => [
29 | \App\Listeners\GoodsGroupDeleted::class,
30 | ],
31 | GoodsDeleted::class => [
32 | \App\Listeners\GoodsDeleted::class,
33 | ],
34 | OrderUpdated::class => [
35 | \App\Listeners\OrderUpdated::class,
36 | ],
37 | ];
38 |
39 | /**
40 | * Register any events for your application.
41 | */
42 | public function boot(): void
43 | {
44 | parent::boot();
45 | }
46 |
47 | /**
48 | * Determine if events and listeners should be automatically discovered.
49 | */
50 | public function shouldDiscoverEvents(): bool
51 | {
52 | return false;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/resources/lang/zh_CN/goods.php:
--------------------------------------------------------------------------------
1 | [
10 | 'Goods' => '商品',
11 | 'goods' => '商品',
12 | ],
13 | 'fields' => [
14 | 'actual_price' => '实际售价',
15 | 'group_id' => '所属分类',
16 | 'api_hook' => '回调事件',
17 | 'buy_prompt' => '购买提示',
18 | 'description' => '商品描述',
19 | 'gd_name' => '商品名称',
20 | 'gd_description' => '商品描述',
21 | 'gd_keywords' => '商品关键字',
22 | 'in_stock' => '库存',
23 | 'ord' => '排序权重',
24 | 'other_ipu_cnf' => '其他输入框配置',
25 | 'picture' => '商品图片',
26 | 'retail_price' => '零售价',
27 | 'sales_volume' => '销量',
28 | 'type' => '商品类型',
29 | 'buy_limit_num' => '限制单次购买最大数量',
30 | 'wholesale_price_cnf' => '批发价配置',
31 | 'automatic_delivery' => '自动发货',
32 | 'manual_processing' => '人工处理',
33 | 'is_open' => '是否上架',
34 | 'coupon_id' => '可用优惠码',
35 | ],
36 | 'options' => [
37 | ],
38 | 'helps' => [
39 | 'retail_price' => '可以不填写,主要用于展示',
40 | 'picture' => '可不上传,为默认图片',
41 | 'in_stock' => '当商品类型为"人工处理"时,手动填写的库存数量才会生效。"自动发货"类型的商品系统会自动识别库存数量',
42 | 'buy_limit_num' => '防止恶意刷库存,0为不限制客户单次下单最大数量',
43 | 'other_ipu_cnf' => '格式为[唯一标识(英文)=输入框名字=是否必填],例如:填写 qq_account=QQ账号=true 表示产品详情页会新增一个 [QQ账号] 输入框,客户可在其中输入 [QQ账号],true 为必填,false 为选填。(一行一个)',
44 | 'wholesale_price_cnf' => '例如:填写 5=3 表示客户购买 5 件或以上时,每件价格为 3 元。一行一个',
45 |
46 | ],
47 | ];
48 |
--------------------------------------------------------------------------------
/resources/lang/zh_TW/goods.php:
--------------------------------------------------------------------------------
1 | [
10 | 'Goods' => '商品',
11 | 'goods' => '商品',
12 | ],
13 | 'fields' => [
14 | 'actual_price' => '實際售價',
15 | 'group_id' => '所屬分類',
16 | 'api_hook' => '回調事件',
17 | 'buy_prompt' => '購買提示',
18 | 'description' => '商品描述',
19 | 'gd_name' => '商品名稱',
20 | 'gd_description' => '商品描述',
21 | 'gd_keywords' => '商品關鍵字',
22 | 'in_stock' => '庫存',
23 | 'ord' => '排序權重',
24 | 'other_ipu_cnf' => '其他輸入框配置',
25 | 'picture' => '商品圖片',
26 | 'retail_price' => '零售價',
27 | 'sales_volume' => '銷量',
28 | 'type' => '商品類型',
29 | 'buy_limit_num' => '限製單次購買最大數量',
30 | 'wholesale_price_cnf' => '批發價配置',
31 | 'automatic_delivery' => '自動發貨',
32 | 'manual_processing' => '人工處理',
33 | 'is_open' => '是否上架',
34 | 'coupon_id' => '可用折扣碼',
35 | ],
36 | 'options' => [
37 | ],
38 | 'helps' => [
39 | 'retail_price' => '可以不填寫,主要用於展示',
40 | 'picture' => '可不上傳,為預設圖片',
41 | 'in_stock' => '當商品類型為"人工處理"時,手動填寫的庫存數量才會生效。"自動發貨"類型的商品系統會自動識別庫存數量',
42 | 'buy_limit_num' => '防止惡意刷庫存,0為不限製客戶單次下單最大數量',
43 | 'other_ipu_cnf' => '格式為[唯一標識(英文)=輸入框名字=是否必填],例如:填寫 line_account=Line賬戶=true 表示產品詳情頁會新增一個 [Line賬戶] 輸入框,客戶可在其中輸入 [Line賬戶],true 為必填,false 為選填。(一行一個)',
44 | 'wholesale_price_cnf' => '例如:填寫 5=3 表示客戶購買 5 件或以上時,每件價格為 3 元。一行一個',
45 |
46 | ],
47 | ];
48 |
--------------------------------------------------------------------------------
/app/Jobs/OrderExpired.php:
--------------------------------------------------------------------------------
1 | orderSN = $orderSN;
50 | }
51 |
52 | /**
53 | * Execute the job.
54 | *
55 | * @return void
56 | */
57 | public function handle()
58 | {
59 | // 如果x分钟后还没支付就算过期
60 | $order = app('Service\OrderService')->detailOrderSN($this->orderSN);
61 | if ($order && $order->status == Order::STATUS_WAIT_PAY) {
62 | app('Service\OrderService')->expiredOrderSN($this->orderSN);
63 | // 回退优惠券
64 | CouponBack::dispatch($order);
65 | }
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/app/Models/Carmis.php:
--------------------------------------------------------------------------------
1 |
42 | * @copyright assimon
43 | *
44 | * @link http://utf8.hk/
45 | */
46 | public static function getStatusMap()
47 | {
48 | return [
49 | self::STATUS_UNSOLD => admin_trans('carmis.fields.status_unsold'),
50 | self::STATUS_SOLD => admin_trans('carmis.fields.status_sold'),
51 | ];
52 | }
53 |
54 | /**
55 | * 关联商品
56 | *
57 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
58 | *
59 | * @author assimon
60 | * @copyright assimon
61 | *
62 | * @link http://utf8.hk/
63 | */
64 | public function goods()
65 | {
66 | return $this->belongsTo(Goods::class, 'goods_id');
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/bootstrap/app.php:
--------------------------------------------------------------------------------
1 | withRouting(
14 | web: __DIR__.'/../routes/web.php',
15 | commands: __DIR__.'/../routes/console.php',
16 | health: '/up',
17 | then: function (): void {
18 | // 自定义路由绑定:支持通过 ID 或 order_sn 查找订单
19 | Route::bind('order', function (string $value) {
20 | // 如果是纯数字,尝试通过 ID 查找
21 | if (is_numeric($value)) {
22 | return \App\Models\Order::findOrFail($value);
23 | }
24 |
25 | // 否则通过 order_sn 查找
26 | return \App\Models\Order::where('order_sn', $value)->firstOrFail();
27 | });
28 | },
29 | )
30 | ->withMiddleware(function (Middleware $middleware): void {
31 | $middleware->alias([
32 | 'dujiaoka.boot' => \App\Http\Middleware\DujiaoBoot::class,
33 | 'dujiaoka.system' => \App\Http\Middleware\DujiaoSystem::class,
34 | 'dujiaoka.pay_gate_way' => \App\Http\Middleware\PayGateWay::class,
35 | ]);
36 | })
37 | ->withProviders([
38 | \App\Providers\EventServiceProvider::class,
39 | ])
40 | ->withExceptions(function (Exceptions $exceptions): void {
41 | //
42 | })->create();
43 |
--------------------------------------------------------------------------------
/app/Models/Pay.php:
--------------------------------------------------------------------------------
1 | admin_trans('pay.fields.method_jump'),
59 | self::METHOD_SCAN => admin_trans('pay.fields.method_scan'),
60 | ];
61 | }
62 |
63 | public static function getClientMap()
64 | {
65 | return [
66 | self::PAY_CLIENT_PC => admin_trans('pay.fields.pay_client_pc'),
67 | self::PAY_CLIENT_MOBILE => admin_trans('pay.fields.pay_client_mobile'),
68 | self::PAY_CLIENT_ALL => admin_trans('pay.fields.pay_client_all'),
69 | ];
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/public/js/filament/forms/components/checkbox-list.js:
--------------------------------------------------------------------------------
1 | function c({livewireId:s}){return{areAllCheckboxesChecked:!1,checkboxListOptions:[],search:"",visibleCheckboxListOptions:[],init(){this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.$nextTick(()=>{this.checkIfAllCheckboxesAreChecked()}),Livewire.hook("commit",({component:e,commit:t,succeed:i,fail:o,respond:h})=>{i(({snapshot:r,effect:l})=>{this.$nextTick(()=>{e.id===s&&(this.checkboxListOptions=Array.from(this.$root.querySelectorAll(".fi-fo-checkbox-list-option")),this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked())})})}),this.$watch("search",()=>{this.updateVisibleCheckboxListOptions(),this.checkIfAllCheckboxesAreChecked()})},checkIfAllCheckboxesAreChecked(){this.areAllCheckboxesChecked=this.visibleCheckboxListOptions.length===this.visibleCheckboxListOptions.filter(e=>e.querySelector("input[type=checkbox]:checked, input[type=checkbox]:disabled")).length},toggleAllCheckboxes(){this.checkIfAllCheckboxesAreChecked();let e=!this.areAllCheckboxesChecked;this.visibleCheckboxListOptions.forEach(t=>{let i=t.querySelector("input[type=checkbox]");i.disabled||(i.checked=e,i.dispatchEvent(new Event("change")))}),this.areAllCheckboxesChecked=e},updateVisibleCheckboxListOptions(){this.visibleCheckboxListOptions=this.checkboxListOptions.filter(e=>["",null,void 0].includes(this.search)||e.querySelector(".fi-fo-checkbox-list-option-label")?.innerText.toLowerCase().includes(this.search.toLowerCase())?!0:e.querySelector(".fi-fo-checkbox-list-option-description")?.innerText.toLowerCase().includes(this.search.toLowerCase()))}}}export{c as default};
2 |
--------------------------------------------------------------------------------
/resources/css/app.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss';
2 | @import '../../vendor/livewire/flux/dist/flux.css';
3 |
4 | @source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
5 | @source '../../storage/framework/views/*.php';
6 | @source '../**/*.blade.php';
7 | @source '../**/*.js';
8 | @source '../views/filament/**/*.blade.php';
9 |
10 | @custom-variant dark (&:where(.dark, .dark *));
11 |
12 | @theme {
13 | --font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
14 | 'Segoe UI Symbol', 'Noto Color Emoji';
15 | }
16 |
17 | @theme {
18 | --color-accent: var(--color-zinc-800);
19 | --color-accent-content: var(--color-zinc-800);
20 | --color-accent-foreground: var(--color-white);
21 | }
22 |
23 | @layer theme {
24 | .dark {
25 | --color-accent: var(--color-white);
26 | --color-accent-content: var(--color-white);
27 | --color-accent-foreground: var(--color-zinc-800);
28 | }
29 | }
30 |
31 | /* Alpine.js x-cloak support */
32 | [x-cloak] {
33 | display: none !important;
34 | }
35 |
36 | /* Remove underline from navbar items */
37 | .no-underline a,
38 | .no-underline button,
39 | [data-flux-component="navbar"] a,
40 | [data-flux-navbar] a {
41 | border-bottom: none !important;
42 | text-decoration: none !important;
43 | box-shadow: none !important;
44 | }
45 |
46 | .no-underline a::after,
47 | .no-underline button::after,
48 | .no-underline a::before,
49 | .no-underline button::before,
50 | [data-flux-component="navbar"] a::after,
51 | [data-flux-navbar] a::after {
52 | display: none !important;
53 | border-bottom: none !important;
54 | content: none !important;
55 | }
56 |
--------------------------------------------------------------------------------
/routes/common/web.php:
--------------------------------------------------------------------------------
1 | ['dujiaoka.boot']], function (): void {
19 | // ====== Livewire 页面路由 ======
20 | // 注意:Livewire 3 直接使用 Route::get() 并将组件类作为目标
21 |
22 | // 首页
23 | Route::get('/', Home::class)->name('home');
24 |
25 | // 购买页
26 | Route::get('/buy/{id}', Buy::class)->name('buy');
27 |
28 | // 订单查询
29 | Route::get('/search-order', SearchOrder::class)->name('search-order');
30 |
31 | // 订单详情
32 | Route::get('/order/{order}', OrderInfo::class)->name('order-info');
33 |
34 | // 支付页
35 | Route::get('/bill/{order}', Bill::class)->name('bill');
36 |
37 | // 二维码支付
38 | Route::get('/qrpay/{order}', QrPay::class)->name('qrpay');
39 |
40 | // 错误页
41 | Route::get('/error', Error::class)->name('error');
42 |
43 | // ====== 保留的后端 API 路由(非页面) ======
44 |
45 | // 创建订单 (POST API)
46 | Route::post('create-order', [OrderController::class, 'createOrder'])->name('create-order');
47 |
48 | // 订单状态检查 (AJAX API)
49 | Route::get('check-order-status/{orderSN}', [OrderController::class, 'checkOrderStatus'])->name('check-order-status');
50 |
51 | // 极验证验证(如果启用)
52 | Route::get('check-geetest', [HomeController::class, 'geetest'])->name('check-geetest');
53 | });
54 |
--------------------------------------------------------------------------------
/database/migrations/0001_01_01_000000_create_users_table.php:
--------------------------------------------------------------------------------
1 | id();
20 | $table->string('name');
21 | $table->string('email')->unique();
22 | $table->timestamp('email_verified_at')->nullable();
23 | $table->string('password');
24 | $table->rememberToken();
25 | $table->timestamps();
26 | });
27 |
28 | Schema::create('password_reset_tokens', function (Blueprint $table) {
29 | $table->string('email')->primary();
30 | $table->string('token');
31 | $table->timestamp('created_at')->nullable();
32 | });
33 |
34 | Schema::create('sessions', function (Blueprint $table) {
35 | $table->string('id')->primary();
36 | $table->foreignId('user_id')->nullable()->index();
37 | $table->string('ip_address', 45)->nullable();
38 | $table->text('user_agent')->nullable();
39 | $table->longText('payload');
40 | $table->integer('last_activity')->index();
41 | });
42 | }
43 |
44 | /**
45 | * Reverse the migrations.
46 | */
47 | public function down(): void
48 | {
49 | Schema::dropIfExists('users');
50 | Schema::dropIfExists('password_reset_tokens');
51 | Schema::dropIfExists('sessions');
52 | }
53 | };
54 |
--------------------------------------------------------------------------------
/tests/Feature/SystemHealthTest.php:
--------------------------------------------------------------------------------
1 | assertTrue(true);
24 |
25 | // 测试是否能连接数据库
26 | $result = \DB::select('SELECT 1');
27 | $this->assertNotEmpty($result);
28 | }
29 |
30 | /**
31 | * 测试模型关系
32 | */
33 | public function test_model_relationships(): void
34 | {
35 | // 测试 GoodsGroup 模型是否存在
36 | $this->assertTrue(class_exists(GoodsGroup::class));
37 |
38 | // 测试 Goods 模型是否存在
39 | $this->assertTrue(class_exists(Goods::class));
40 |
41 | // 测试 Order 模型是否存在
42 | $this->assertTrue(class_exists(Order::class));
43 |
44 | // 测试 Pay 模型是否存在
45 | $this->assertTrue(class_exists(Pay::class));
46 | }
47 |
48 | /**
49 | * 测试路由是否正常
50 | */
51 | public function test_routes_are_accessible(): void
52 | {
53 | // 测试前台首页
54 | $response = $this->get('/');
55 | $response->assertStatus(200);
56 | }
57 |
58 | /**
59 | * 测试服务提供者注册
60 | */
61 | public function test_service_providers_registered(): void
62 | {
63 | // 测试 GoodsService 是否注册
64 | $this->assertTrue(app()->bound('Service\GoodsService'));
65 |
66 | // 测试 OrderService 是否注册
67 | $this->assertTrue(app()->bound('Service\OrderService'));
68 |
69 | // 测试 PayService 是否注册
70 | $this->assertTrue(app()->bound('Service\PayService'));
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/app/Livewire/Pages/OrderInfo.php:
--------------------------------------------------------------------------------
1 | order = $order->load(['goods', 'pay']);
23 | }
24 |
25 | public function getStatusBadgeColor(): string
26 | {
27 | return match ($this->order->status) {
28 | Order::STATUS_WAIT_PAY => 'amber',
29 | Order::STATUS_PENDING => 'blue',
30 | Order::STATUS_PROCESSING => 'blue',
31 | Order::STATUS_COMPLETED => 'green',
32 | Order::STATUS_FAILURE => 'red',
33 | Order::STATUS_EXPIRED => 'zinc',
34 | Order::STATUS_ABNORMAL => 'red',
35 | default => 'zinc',
36 | };
37 | }
38 |
39 | public function getStatusText(): string
40 | {
41 | return match ($this->order->status) {
42 | Order::STATUS_WAIT_PAY => '待支付',
43 | Order::STATUS_PENDING => '待处理',
44 | Order::STATUS_PROCESSING => '处理中',
45 | Order::STATUS_COMPLETED => '已完成',
46 | Order::STATUS_FAILURE => '失败',
47 | Order::STATUS_EXPIRED => '已过期',
48 | Order::STATUS_ABNORMAL => '异常',
49 | default => '未知',
50 | };
51 | }
52 |
53 | public function render(SeoService $seoService)
54 | {
55 | $seoData = $seoService->getDefaultSeoData('订单详情', true);
56 | $seoData['noindex'] = true;
57 |
58 | return view('livewire.pages.order-info')->with($seoData);
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/resources/lang/zh_TW/system-setting.php:
--------------------------------------------------------------------------------
1 | [
10 | 'SystemSetting' => '系統設定',
11 | 'system_setting' => '系統設定',
12 | 'base_setting' => '基本設定',
13 | 'mail_setting' => '信箱服務',
14 | 'order_push_setting' => '訂單推送配置',
15 | 'geetest' => '極驗驗證',
16 | ],
17 |
18 | 'fields' => [
19 | 'title' => '網站標題',
20 | 'text_logo' => '文字LOGO',
21 | 'img_logo' => '圖片LOGO',
22 | 'keywords' => '網站關鍵詞',
23 | 'description' => '網站描述',
24 | 'notice' => '站點公告',
25 | 'footer' => '頁尾自訂代碼',
26 | 'manage_email' => '管理員信箱',
27 | 'is_open_anti_red' => '是否開啟Wechat/QQ防紅',
28 | 'is_open_img_code' => '是否開啟圖形驗證碼',
29 | 'is_open_search_pwd' => '是否開啟查詢密碼',
30 | 'is_open_server_jiang' => '是否開啟server醬',
31 | 'server_jiang_token' => 'server醬通訊token',
32 | 'is_open_telegram_push' => '是否開啟Telegram推送',
33 | 'telegram_userid' => 'Telegram用戶id',
34 | 'telegram_bot_token' => 'Telegram通訊token',
35 | 'language' => '站點語言',
36 | 'order_expire_time' => '訂單逾期時間(分鐘)',
37 |
38 | 'driver' => '信箱驅動',
39 | 'host' => 'smtp伺服器地址',
40 | 'port' => '通訊埠',
41 | 'username' => '賬戶',
42 | 'password' => '密碼',
43 | 'encryption' => '協議',
44 | 'from_address' => '發件地址',
45 | 'from_name' => '發件名稱',
46 |
47 | 'geetest_id' => '極驗id',
48 | 'geetest_key' => '極驗key',
49 | 'is_open_geetest' => '是否開啟極驗',
50 | ],
51 | 'options' => [
52 | ],
53 | 'rule_messages' => [
54 | 'save_system_setting_success' => '系統配置套用成功!',
55 | 'change_reboot_php_worker' => '修改部分配置需要重新啓動[supervisor]或php進程管理工具才會生效,例如信箱服務,server醬等。',
56 | ],
57 | ];
58 |
--------------------------------------------------------------------------------
/app/Http/Middleware/DujiaoBoot.php:
--------------------------------------------------------------------------------
1 | header('user-agent');
26 | $nowUri = site_url().$request->path();
27 | $tplPath = 'common/notencent';
28 | if (
29 | (strpos($userAgent, 'QQ/')
30 | ||
31 | strpos($userAgent, 'MicroMessenger') !== false)
32 | &&
33 | dujiaoka_config_get('is_open_anti_red', BaseModel::STATUS_OPEN) == BaseModel::STATUS_OPEN
34 | ) {
35 | return response()->view($tplPath, ['nowUri' => $nowUri]);
36 | }
37 | // 语言检测
38 | $lang = dujiaoka_config_get('language', 'zh_CN');
39 | app()->setLocale($lang);
40 | // 极验
41 | $geetest = dujiaoka_config_get('is_open_geetest', BaseModel::STATUS_CLOSE);
42 | if ($geetest == BaseModel::STATUS_OPEN) {
43 | $geetestConfig = [
44 | 'key' => dujiaoka_config_get('geetest_key'),
45 | 'id' => dujiaoka_config_get('geetest_id'),
46 | 'lang' => $lang,
47 | ];
48 | // 覆盖 配置
49 | config([
50 | 'geetest' => array_merge(config('mail'), $geetestConfig),
51 | ]);
52 | // 重新注册服务
53 | (new GeetestServiceProvider(app()))->register();
54 | }
55 |
56 | return $next($request);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/config/hashing.php:
--------------------------------------------------------------------------------
1 | 'bcrypt',
24 |
25 | /*
26 | |--------------------------------------------------------------------------
27 | | Bcrypt Options
28 | |--------------------------------------------------------------------------
29 | |
30 | | Here you may specify the configuration options that should be used when
31 | | passwords are hashed using the Bcrypt algorithm. This will allow you
32 | | to control the amount of time it takes to hash the given password.
33 | |
34 | */
35 |
36 | 'bcrypt' => [
37 | 'rounds' => env('BCRYPT_ROUNDS', 10),
38 | ],
39 |
40 | /*
41 | |--------------------------------------------------------------------------
42 | | Argon Options
43 | |--------------------------------------------------------------------------
44 | |
45 | | Here you may specify the configuration options that should be used when
46 | | passwords are hashed using the Argon algorithm. These will allow you
47 | | to control the amount of time it takes to hash the given password.
48 | |
49 | */
50 |
51 | 'argon' => [
52 | 'memory' => 1024,
53 | 'threads' => 2,
54 | 'time' => 2,
55 | ],
56 |
57 | ];
58 |
--------------------------------------------------------------------------------
/config/broadcasting.php:
--------------------------------------------------------------------------------
1 | env('BROADCAST_DRIVER', 'null'),
24 |
25 | /*
26 | |--------------------------------------------------------------------------
27 | | Broadcast Connections
28 | |--------------------------------------------------------------------------
29 | |
30 | | Here you may define all of the broadcast connections that will be used
31 | | to broadcast events to other systems or over websockets. Samples of
32 | | each available type of connection are provided inside this array.
33 | |
34 | */
35 |
36 | 'connections' => [
37 |
38 | 'pusher' => [
39 | 'driver' => 'pusher',
40 | 'key' => env('PUSHER_APP_KEY'),
41 | 'secret' => env('PUSHER_APP_SECRET'),
42 | 'app_id' => env('PUSHER_APP_ID'),
43 | 'options' => [
44 | 'cluster' => env('PUSHER_APP_CLUSTER'),
45 | 'useTLS' => true,
46 | ],
47 | ],
48 |
49 | 'redis' => [
50 | 'driver' => 'redis',
51 | 'connection' => 'default',
52 | ],
53 |
54 | 'log' => [
55 | 'driver' => 'log',
56 | ],
57 |
58 | 'null' => [
59 | 'driver' => 'null',
60 | ],
61 |
62 | ],
63 |
64 | ];
65 |
--------------------------------------------------------------------------------
/app/Filament/Resources/Goods/Pages/ListGoods.php:
--------------------------------------------------------------------------------
1 | Tab::make('全部')
33 | ->badge(Goods::count()),
34 | ];
35 |
36 | // 获取所有商品分类
37 | $groups = GoodsGroup::query()
38 | ->orderBy('ord', 'desc')
39 | ->orderBy('id', 'asc')
40 | ->get();
41 |
42 | foreach ($groups as $group) {
43 | $count = Goods::where('group_id', $group->id)->count();
44 |
45 | $tabs['group_'.$group->id] = Tab::make($group->gp_name)
46 | ->badge($count)
47 | ->badgeColor('info')
48 | ->modifyQueryUsing(fn (Builder $query) => $query->where('group_id', $group->id));
49 | }
50 |
51 | // 添加未分类商品 Tab
52 | $uncategorizedCount = Goods::whereNull('group_id')->count();
53 | if ($uncategorizedCount > 0) {
54 | $tabs['uncategorized'] = Tab::make('未分类')
55 | ->badge($uncategorizedCount)
56 | ->badgeColor('warning')
57 | ->modifyQueryUsing(fn (Builder $query) => $query->whereNull('group_id'));
58 | }
59 |
60 | return $tabs;
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/public/fonts/filament/filament/inter/index.css:
--------------------------------------------------------------------------------
1 | @font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-cyrillic-ext-wght-normal-IYF56FF6.woff2") format("woff2-variations");unicode-range:U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-cyrillic-wght-normal-JEOLYBOO.woff2") format("woff2-variations");unicode-range:U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-greek-ext-wght-normal-EOVOK2B5.woff2") format("woff2-variations");unicode-range:U+1F00-1FFF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-greek-wght-normal-IRE366VL.woff2") format("woff2-variations");unicode-range:U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-vietnamese-wght-normal-CE5GGD3W.woff2") format("woff2-variations");unicode-range:U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-latin-ext-wght-normal-HA22NDSG.woff2") format("woff2-variations");unicode-range:U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF}@font-face{font-family:Inter Variable;font-style:normal;font-display:swap;font-weight:100 900;src:url("./inter-latin-wght-normal-NRMW37G5.woff2") format("woff2-variations");unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
2 |
--------------------------------------------------------------------------------
/app/Service/CouponService.php:
--------------------------------------------------------------------------------
1 |
22 | * @copyright assimon
23 | *
24 | * @link http://utf8.hk/
25 | */
26 | public function withHasGoods(string $coupon, int $goodsID)
27 | {
28 | $coupon = Coupon::query()->whereHas('goods', function ($query) use ($goodsID): void {
29 | $query->where('goods_id', $goodsID);
30 | })->where('is_open', Coupon::STATUS_OPEN)->where('coupon', $coupon)->first();
31 |
32 | return $coupon;
33 | }
34 |
35 | /**
36 | * 设置优惠券已使用
37 | */
38 | public function used(string $coupon): bool
39 | {
40 | return Coupon::query()
41 | ->where('coupon', $coupon)
42 | ->update(['is_use' => Coupon::STATUS_USE]);
43 | }
44 |
45 | /**
46 | * 设置优惠券使用次数 -1
47 | *
48 | * @return int
49 | *
50 | * @author assimon
51 | * @copyright assimon
52 | *
53 | * @link http://utf8.hk/
54 | */
55 | public function retDecr(string $coupon)
56 | {
57 | return Coupon::query()
58 | ->where('coupon', $coupon)
59 | ->decrement('ret', 1);
60 | }
61 |
62 | /**
63 | * 设置优惠券次数+1
64 | *
65 | * @return int
66 | *
67 | * @author assimon
68 | * @copyright assimon
69 | *
70 | * @link http://utf8.hk/
71 | */
72 | public function retIncrByID(int $id)
73 | {
74 | return Coupon::query()->where('id', $id)->increment('ret', 1);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/Jobs/ServerJiang.php:
--------------------------------------------------------------------------------
1 | order = $order;
48 | }
49 |
50 | /**
51 | * Execute the job.
52 | *
53 | * @return void
54 | */
55 | public function handle()
56 | {
57 | $postdata = http_build_query([
58 | 'text' => __('dujiaoka.prompt.new_order_push').":{$this->order['ord_title']}",
59 | 'desp' => '
60 | - '.__('order.fields.title').":{$this->order->title}
61 | - ".__('order.fields.order_sn').":{$this->order->order_sn}
62 | - ".__('order.fields.email').":{$this->order->email}
63 | - ".__('order.fields.actual_price').":{$this->order->actual_price}
64 | ",
65 | ]);
66 | $opts = [
67 | 'http' => [
68 | 'method' => 'POST',
69 | 'header' => 'Content-type: application/x-www-form-urlencoded',
70 | 'content' => $postdata,
71 | ],
72 | ];
73 | $context = stream_context_create($opts);
74 | $apiToken = dujiaoka_config_get('server_jiang_token');
75 | file_get_contents('https://sctapi.ftqq.com/'.$apiToken.'.send', false, $context);
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/resources/lang/zh_CN/system-setting.php:
--------------------------------------------------------------------------------
1 | [
10 | 'SystemSetting' => '系统设置',
11 | 'system_setting' => '系统设置',
12 | 'base_setting' => '基本设置',
13 | 'mail_setting' => '邮件服务',
14 | 'order_push_setting' => '订单推送配置',
15 | 'geetest' => '极验验证',
16 | ],
17 |
18 | 'fields' => [
19 | 'title' => '网站标题',
20 | 'text_logo' => '文字LOGO',
21 | 'img_logo' => '图片LOGO',
22 | 'keywords' => '网站关键词',
23 | 'description' => '网站描述',
24 | 'notice' => '站点公告',
25 | 'footer' => '页脚自定义代码',
26 | 'manage_email' => '管理员邮箱',
27 | 'is_open_anti_red' => '是否开启微信/QQ防红',
28 | 'is_open_img_code' => '是否开启图形验证码',
29 | 'is_open_search_pwd' => '是否开启查询密码',
30 |
31 | 'is_open_server_jiang' => '是否开启server酱',
32 | 'server_jiang_token' => 'server酱通讯token',
33 | 'is_open_telegram_push' => '是否开启Telegram推送',
34 | 'telegram_userid' => 'Telegram用户id',
35 | 'telegram_bot_token' => 'Telegram通讯token',
36 | 'is_open_bark_push' => '是否开启Bark推送',
37 | 'is_open_bark_push_url' => '是否推送订单URL',
38 | 'bark_server' => 'Bark服务器',
39 | 'bark_token' => 'Bark通讯Token',
40 | 'is_open_qywxbot_push' => '是否开启企业微信Bot推送',
41 | 'qywxbot_key' => '企业微信Bot通讯Key',
42 |
43 | 'language' => '站点语言',
44 | 'order_expire_time' => '订单过期时间(分钟)',
45 |
46 | 'driver' => '邮件驱动',
47 | 'host' => 'smtp服务器地址',
48 | 'port' => '端口',
49 | 'username' => '账号',
50 | 'password' => '密码',
51 | 'encryption' => '协议',
52 | 'from_address' => '发件地址',
53 | 'from_name' => '发件名称',
54 |
55 | 'geetest_id' => '极验id',
56 | 'geetest_key' => '极验key',
57 | 'is_open_geetest' => '是否开启极验',
58 | ],
59 | 'options' => [
60 | ],
61 | 'rule_messages' => [
62 | 'save_system_setting_success' => '系统配置保存成功!',
63 | 'change_reboot_php_worker' => '修改部分配置需要重启[supervisor]或php进程管理工具才会生效,例如邮件服务,server酱等。',
64 | ],
65 | ];
66 |
--------------------------------------------------------------------------------
/database/migrations/0001_01_01_000002_create_jobs_table.php:
--------------------------------------------------------------------------------
1 | id();
20 | $table->string('queue')->index();
21 | $table->longText('payload');
22 | $table->unsignedTinyInteger('attempts');
23 | $table->unsignedInteger('reserved_at')->nullable();
24 | $table->unsignedInteger('available_at');
25 | $table->unsignedInteger('created_at');
26 | });
27 |
28 | Schema::create('job_batches', function (Blueprint $table) {
29 | $table->string('id')->primary();
30 | $table->string('name');
31 | $table->integer('total_jobs');
32 | $table->integer('pending_jobs');
33 | $table->integer('failed_jobs');
34 | $table->longText('failed_job_ids');
35 | $table->mediumText('options')->nullable();
36 | $table->integer('cancelled_at')->nullable();
37 | $table->integer('created_at');
38 | $table->integer('finished_at')->nullable();
39 | });
40 |
41 | Schema::create('failed_jobs', function (Blueprint $table) {
42 | $table->id();
43 | $table->string('uuid')->unique();
44 | $table->text('connection');
45 | $table->text('queue');
46 | $table->longText('payload');
47 | $table->longText('exception');
48 | $table->timestamp('failed_at')->useCurrent();
49 | });
50 | }
51 |
52 | /**
53 | * Reverse the migrations.
54 | */
55 | public function down(): void
56 | {
57 | Schema::dropIfExists('jobs');
58 | Schema::dropIfExists('job_batches');
59 | Schema::dropIfExists('failed_jobs');
60 | }
61 | };
62 |
--------------------------------------------------------------------------------
/database/migrations/2025_11_01_013549_create_goods_table.php:
--------------------------------------------------------------------------------
1 | id();
20 | $table->unsignedInteger('group_id')->comment('所属分类id');
21 | $table->string('gd_name', 200)->comment('商品名称');
22 | $table->string('gd_description', 200)->comment('商品描述');
23 | $table->string('gd_keywords', 200)->comment('商品关键字');
24 | $table->text('picture')->nullable()->comment('商品图片');
25 | $table->decimal('retail_price', 10, 2)->default(0)->comment('零售价');
26 | $table->decimal('actual_price', 10, 2)->default(0)->comment('实际售价');
27 | $table->integer('in_stock')->default(0)->comment('库存');
28 | $table->integer('sales_volume')->default(0)->comment('销量');
29 | $table->integer('ord')->default(1)->comment('排序权重 越大越靠前');
30 | $table->integer('buy_limit_num')->default(0)->comment('限制单次购买最大数量,0为不限制');
31 | $table->text('buy_prompt')->nullable()->comment('购买提示');
32 | $table->text('description')->nullable()->comment('商品描述');
33 | $table->tinyInteger('type')->default(1)->comment('商品类型 1自动发货 2人工处理');
34 | $table->text('wholesale_price_cnf')->nullable()->comment('批发价配置');
35 | $table->text('other_ipu_cnf')->nullable()->comment('其他输入框配置');
36 | $table->text('api_hook')->nullable()->comment('回调事件');
37 | $table->boolean('is_open')->default(true)->comment('是否启用,1是 0否');
38 | $table->timestamps();
39 | $table->softDeletes();
40 | });
41 | }
42 |
43 | /**
44 | * Reverse the migrations.
45 | */
46 | public function down(): void
47 | {
48 | Schema::dropIfExists('goods');
49 | }
50 | };
51 |
--------------------------------------------------------------------------------
/app/Filament/Imports/CarmisImporter.php:
--------------------------------------------------------------------------------
1 | label('商品ID')
25 | ->requiredMapping()
26 | ->numeric()
27 | ->rules(['required', 'integer', 'exists:goods,id']),
28 | ImportColumn::make('carmi')
29 | ->label('卡密内容')
30 | ->requiredMapping()
31 | ->rules(['required', 'max:5000']),
32 | ImportColumn::make('status')
33 | ->label('状态')
34 | ->numeric()
35 | ->rules(['integer'])
36 | ->example('1')
37 | ->fillRecordUsing(function (Carmis $record, $state): void {
38 | $record->status = $state ?? Carmis::STATUS_UNSOLD;
39 | }),
40 | ImportColumn::make('is_loop')
41 | ->label('循环使用')
42 | ->boolean()
43 | ->rules(['boolean'])
44 | ->example('0')
45 | ->fillRecordUsing(function (Carmis $record, $state): void {
46 | $record->is_loop = $state ?? false;
47 | }),
48 | ];
49 | }
50 |
51 | public function resolveRecord(): Carmis
52 | {
53 | return new Carmis;
54 | }
55 |
56 | public static function getCompletedNotificationBody(Import $import): string
57 | {
58 | $body = '卡密导入已完成,成功导入 '.Number::format($import->successful_rows).' 条记录。';
59 |
60 | if ($failedRowsCount = $import->getFailedRowsCount()) {
61 | $body .= ' 失败 '.Number::format($failedRowsCount).' 条记录。';
62 | }
63 |
64 | return $body;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/database/factories/CarmisFactory.php:
--------------------------------------------------------------------------------
1 |
16 | */
17 | class CarmisFactory extends Factory
18 | {
19 | /**
20 | * Define the model's default state.
21 | *
22 | * @return array
23 | */
24 | public function definition(): array
25 | {
26 | $carmiTypes = [
27 | // 账号密码格式
28 | fn () => '账号: '.fake()->userName()."\n密码: ".fake()->password(8, 16),
29 | // 激活码格式
30 | fn () => strtoupper(fake()->bothify('????-####-????-####')),
31 | // 兑换码格式
32 | fn () => strtoupper(fake()->bothify('??########??')),
33 | // 邮箱密码格式
34 | fn () => fake()->email()."\n".fake()->password(8, 16),
35 | // 卡号密码格式
36 | fn () => '卡号: '.fake()->numerify('################')."\n密码: ".fake()->numerify('######'),
37 | ];
38 |
39 | return [
40 | 'goods_id' => Goods::factory()->automatic(),
41 | 'carmi' => fake()->randomElement($carmiTypes)(),
42 | 'status' => fake()->randomElement([Carmis::STATUS_UNSOLD, Carmis::STATUS_SOLD]),
43 | 'is_loop' => fake()->boolean(10), // 10% 循环使用
44 | ];
45 | }
46 |
47 | /**
48 | * 未售出状态
49 | */
50 | public function unsold(): static
51 | {
52 | return $this->state(fn (array $attributes) => [
53 | 'status' => Carmis::STATUS_UNSOLD,
54 | ]);
55 | }
56 |
57 | /**
58 | * 已售出状态
59 | */
60 | public function sold(): static
61 | {
62 | return $this->state(fn (array $attributes) => [
63 | 'status' => Carmis::STATUS_SOLD,
64 | ]);
65 | }
66 |
67 | /**
68 | * 循环使用
69 | */
70 | public function loop(): static
71 | {
72 | return $this->state(fn (array $attributes) => [
73 | 'is_loop' => true,
74 | ]);
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/app/Filament/Resources/Pays/PayResource.php:
--------------------------------------------------------------------------------
1 | ListPays::route('/'),
61 | 'create' => CreatePay::route('/create'),
62 | 'edit' => EditPay::route('/{record}/edit'),
63 | ];
64 | }
65 |
66 | public static function getRecordRouteBindingEloquentQuery(): Builder
67 | {
68 | return parent::getRecordRouteBindingEloquentQuery()
69 | ->withoutGlobalScopes([
70 | SoftDeletingScope::class,
71 | ]);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/database/factories/CouponFactory.php:
--------------------------------------------------------------------------------
1 |
15 | */
16 | class CouponFactory extends Factory
17 | {
18 | /**
19 | * Define the model's default state.
20 | *
21 | * @return array
22 | */
23 | public function definition(): array
24 | {
25 | $type = fake()->numberBetween(1, 4);
26 | $coupon = match ($type) {
27 | 1 => strtoupper(fake()->bothify('??##??##')),
28 | 2 => 'VIP'.fake()->numerify('######'),
29 | 3 => strtoupper(fake()->lexify('??????')),
30 | 4 => fake()->bothify('DISCOUNT####'),
31 | };
32 |
33 | return [
34 | 'coupon' => fake()->unique()->regexify('[A-Z0-9]{10}'),
35 | 'discount' => fake()->randomFloat(2, 5, 100),
36 | 'is_use' => Coupon::STATUS_UNUSED,
37 | 'ret' => fake()->numberBetween(0, 100), // 0 = 无限制
38 | 'is_open' => fake()->boolean(85),
39 | ];
40 | }
41 |
42 | /**
43 | * 未使用状态
44 | */
45 | public function unused(): static
46 | {
47 | return $this->state(fn (array $attributes) => [
48 | 'is_use' => Coupon::STATUS_UNUSED,
49 | ]);
50 | }
51 |
52 | /**
53 | * 已使用状态
54 | */
55 | public function used(): static
56 | {
57 | return $this->state(fn (array $attributes) => [
58 | 'is_use' => Coupon::STATUS_USE,
59 | 'ret' => 0,
60 | ]);
61 | }
62 |
63 | /**
64 | * 无限次使用
65 | */
66 | public function unlimited(): static
67 | {
68 | return $this->state(fn (array $attributes) => [
69 | 'ret' => 0,
70 | ]);
71 | }
72 |
73 | /**
74 | * 大额优惠券
75 | */
76 | public function large(): static
77 | {
78 | return $this->state(fn (array $attributes) => [
79 | 'discount' => fake()->randomFloat(2, 50, 200),
80 | ]);
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/app/Filament/Resources/Orders/Widgets/OrderStatsOverview.php:
--------------------------------------------------------------------------------
1 | count();
28 |
29 | // 已完成订单的平均金额
30 | $averagePrice = Order::where('status', Order::STATUS_COMPLETED)
31 | ->avg('actual_price') ?? 0;
32 |
33 | // 获取最近7天的订单趋势数据
34 | $recentOrders = Order::selectRaw('DATE(created_at) as date, COUNT(*) as count')
35 | ->where('created_at', '>=', now()->subDays(6))
36 | ->groupBy('date')
37 | ->orderBy('date')
38 | ->pluck('count')
39 | ->toArray();
40 |
41 | // 填充缺失的天数数据
42 | $chartData = array_pad($recentOrders, 7, 0);
43 | $chartData = array_slice($chartData, -7);
44 |
45 | return [
46 | Stat::make('订单总数', number_format((int) $totalOrders))
47 | ->description('所有订单')
48 | ->descriptionIcon('heroicon-o-shopping-cart', IconPosition::Before)
49 | ->color('primary')
50 | ->chart($chartData),
51 |
52 | Stat::make('未完成订单', number_format((int) $openOrders))
53 | ->description('待支付、待处理、处理中')
54 | ->descriptionIcon('heroicon-o-clock', IconPosition::Before)
55 | ->color('warning'),
56 |
57 | Stat::make('平均订单金额', '¥'.number_format((float) $averagePrice, 2))
58 | ->description('已完成订单的平均金额')
59 | ->descriptionIcon('heroicon-o-currency-dollar', IconPosition::Before)
60 | ->color('success'),
61 | ];
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/app/Livewire/Pages/Home.php:
--------------------------------------------------------------------------------
1 | selectedGroup = $groupId;
32 |
33 | // 重置搜索
34 | $this->search = '';
35 | }
36 |
37 | public function render(GoodsService $goodsService, SeoService $seoService)
38 | {
39 | // 获取所有分组用于 Tab 导航
40 | $allGroups = $goodsService->withGroup();
41 |
42 | // 获取商品分组及商品
43 | $goodsGroups = $allGroups;
44 |
45 | // 如果选择了特定分组,只保留该分组
46 | if ($this->selectedGroup !== null) {
47 | $goodsGroups = $goodsGroups->filter(function ($group) {
48 | return (int) $group->id === (int) $this->selectedGroup;
49 | })->values();
50 | }
51 |
52 | // 如果有搜索关键词,过滤商品(在所有分组中搜索)
53 | if ($this->search) {
54 | $goodsGroups = $goodsGroups->map(function ($group) {
55 | $group->setRelation('goods', $group->goods->filter(function ($goods) {
56 | return str_contains(strtolower($goods->gd_name), strtolower($this->search))
57 | || str_contains(strtolower($goods->gd_description ?? ''), strtolower($this->search));
58 | }));
59 |
60 | return $group;
61 | })->filter(function ($group) {
62 | return $group->goods->count() > 0;
63 | })->values();
64 | }
65 |
66 | // 获取 SEO 数据
67 | $seoData = $seoService->getHomeSeoData();
68 |
69 | return view('livewire.pages.home', [
70 | 'goodsGroups' => $goodsGroups,
71 | 'allGroups' => $allGroups,
72 | ])->with($seoData);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/app/Filament/Resources/Emailtpls/EmailtplResource.php:
--------------------------------------------------------------------------------
1 | ListEmailtpls::route('/'),
61 | 'create' => CreateEmailtpl::route('/create'),
62 | 'edit' => EditEmailtpl::route('/{record}/edit'),
63 | ];
64 | }
65 |
66 | public static function getRecordRouteBindingEloquentQuery(): Builder
67 | {
68 | return parent::getRecordRouteBindingEloquentQuery()
69 | ->withoutGlobalScopes([
70 | SoftDeletingScope::class,
71 | ]);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/Filament/Resources/GoodsGroups/GoodsGroupResource.php:
--------------------------------------------------------------------------------
1 | ListGoodsGroups::route('/'),
61 | 'create' => CreateGoodsGroup::route('/create'),
62 | 'edit' => EditGoodsGroup::route('/{record}/edit'),
63 | ];
64 | }
65 |
66 | public static function getRecordRouteBindingEloquentQuery(): Builder
67 | {
68 | return parent::getRecordRouteBindingEloquentQuery()
69 | ->withoutGlobalScopes([
70 | SoftDeletingScope::class,
71 | ]);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/Jobs/ApiHook.php:
--------------------------------------------------------------------------------
1 | order = $order;
55 | $this->goodsService = app('Service\GoodsService');
56 | }
57 |
58 | /**
59 | * Execute the job.
60 | *
61 | * @return void
62 | */
63 | public function handle()
64 | {
65 | $goodInfo = $this->goodsService->detail($this->order->goods_id);
66 | // 判断是否有配置支付回调
67 | if (empty($goodInfo->api_hook)) {
68 | return;
69 | }
70 | $postdata = [
71 | 'title' => $this->order->title,
72 | 'order_sn' => $this->order->order_sn,
73 | 'email' => $this->order->email,
74 | 'actual_price' => $this->order->actual_price,
75 | 'order_info' => $this->order->info,
76 | 'good_id' => $goodInfo->id,
77 | 'gd_name' => $goodInfo->gd_name,
78 |
79 | ];
80 |
81 | $opts = [
82 | 'http' => [
83 | 'method' => 'POST',
84 | 'header' => 'Content-type: application/json',
85 | 'content' => json_encode($postdata, JSON_UNESCAPED_UNICODE),
86 | ],
87 | ];
88 | $context = stream_context_create($opts);
89 | file_get_contents($goodInfo->api_hook, false, $context);
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/resources/views/vendor/geetest/geetest.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | loading...
5 |
6 | @define use Illuminate\Support\Facades\Config
7 |
51 |
56 |
--------------------------------------------------------------------------------
/app/Filament/Resources/Coupons/Schemas/CouponForm.php:
--------------------------------------------------------------------------------
1 | columns(1)
22 | ->components([
23 | Section::make('优惠券基本信息')
24 | ->columnSpan('full')
25 | ->schema([
26 | TextInput::make('coupon')
27 | ->label('优惠券码')
28 | ->required()
29 | ->unique(ignoreRecord: true)
30 | ->maxLength(150)
31 | ->helperText('优惠券的唯一标识码'),
32 |
33 | TextInput::make('discount')
34 | ->label('优惠金额')
35 | ->numeric()
36 | ->required()
37 | ->prefix('¥')
38 | ->step(0.01)
39 | ->helperText('减免的金额'),
40 |
41 | TextInput::make('ret')
42 | ->label('剩余使用次数')
43 | ->numeric()
44 | ->default(0)
45 | ->helperText('0表示无限制使用'),
46 |
47 | Toggle::make('is_open')
48 | ->label('是否启用')
49 | ->default(true)
50 | ->inline(false),
51 | ]),
52 |
53 | Section::make('关联商品')
54 | ->columnSpan('full')
55 | ->schema([
56 | Select::make('goods')
57 | ->label('适用商品')
58 | ->relationship('goods', 'gd_name')
59 | ->multiple()
60 | ->preload()
61 | ->searchable()
62 | ->helperText('不选择则适用于所有商品'),
63 | ]),
64 | ]);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/Jobs/MailSend.php:
--------------------------------------------------------------------------------
1 | to = $to;
50 | $this->title = $title;
51 | $this->content = $content;
52 | }
53 |
54 | /**
55 | * Execute the job.
56 | *
57 | * @return void
58 | */
59 | public function handle()
60 | {
61 | $body = $this->content;
62 | $title = $this->title;
63 | $sysConfig = cache('system-setting');
64 | $mailConfig = [
65 | 'driver' => $sysConfig['driver'] ?? 'smtp',
66 | 'host' => $sysConfig['host'] ?? '',
67 | 'port' => $sysConfig['port'] ?? '465',
68 | 'username' => $sysConfig['username'] ?? '',
69 | 'from' => [
70 | 'address' => $sysConfig['from_address'] ?? '',
71 | 'name' => $sysConfig['from_name'] ?? '独角发卡',
72 | ],
73 | 'password' => $sysConfig['password'] ?? '',
74 | 'encryption' => $sysConfig['encryption'] ?? '',
75 | ];
76 | $to = $this->to;
77 | // 覆盖 mail 配置
78 | config([
79 | 'mail' => array_merge(config('mail'), $mailConfig),
80 | ]);
81 | // 重新注册驱动
82 | (new MailServiceProvider(app()))->register();
83 | Mail::send(['html' => 'email.mail'], ['body' => $body], function ($message) use ($to, $title): void {
84 | $message->to($to)->subject($title);
85 | });
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Pay/PayjsController.php:
--------------------------------------------------------------------------------
1 | loadGateWay($orderSN, $payway);
22 | // 构造订单基础信息
23 | $data = [
24 | 'body' => $this->order->order_sn, // 订单标题
25 | 'total_fee' => bcmul((string) $this->order->actual_price, '100', 0), // 订单金额
26 | 'out_trade_no' => $this->order->order_sn, // 订单号
27 | 'notify_url' => url($this->payGateway->pay_handleroute.'/notify_url'),
28 | ];
29 | config(['payjs.mchid' => $this->payGateway->merchant_id, 'payjs.key' => $this->payGateway->merchant_pem]);
30 | switch ($payway) {
31 | case 'payjswescan':
32 | // QR code payments are handled by QrPay Livewire component
33 | return redirect(route('qrpay', ['order' => $this->order->order_sn]));
34 | }
35 | } catch (RuleValidationException $exception) {
36 | return $this->err($exception->getMessage());
37 | }
38 | }
39 |
40 | public function notifyUrl(Request $request)
41 | {
42 | $orderSN = $request->input('out_trade_no');
43 | $order = $this->orderService->detailOrderSN($orderSN);
44 | if (! $order) {
45 | return 'error';
46 | }
47 | $payGateway = $this->payService->detail($order->pay_id);
48 | if (! $payGateway) {
49 | return 'error';
50 | }
51 | if ($payGateway->pay_handleroute != '/pay/payjs') {
52 | return 'fail';
53 | }
54 | config(['payjs.mchid' => $payGateway->merchant_id, 'payjs.key' => $payGateway->merchant_pem]);
55 | $notify_info = Payjs::notify();
56 | $totalFee = (float) bcdiv((string) $notify_info['total_fee'], '100', 2);
57 | $this->orderProcessService->completedOrder($notify_info['out_trade_no'], $totalFee, $notify_info['payjs_order_id']);
58 |
59 | return 'success';
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/app/Livewire/Pages/QrPay.php:
--------------------------------------------------------------------------------
1 | orderData = Order::with(['goods', 'pay'])->where('order_sn', $order)->firstOrFail();
29 |
30 | // 检查订单状态
31 | if ($this->orderData->status == Order::STATUS_EXPIRED) {
32 | session()->flash('error_message', '订单已过期');
33 | $this->redirect(route('search-order'));
34 |
35 | return;
36 | }
37 |
38 | if ($this->orderData->status == Order::STATUS_COMPLETED) {
39 | // 订单已支付,跳转到订单详情
40 | $this->redirect(route('order-info', ['order' => $this->orderData->id]));
41 |
42 | return;
43 | }
44 |
45 | // 生成支付二维码
46 | try {
47 | $paymentData = $payService->pay($this->orderData);
48 | $this->qrcodeUrl = $paymentData['qrcode'] ?? '';
49 | } catch (\Exception $e) {
50 | session()->flash('error_message', '生成支付二维码失败:'.$e->getMessage());
51 | $this->redirect(route('bill', ['order' => $this->orderData->order_sn]));
52 | }
53 | }
54 |
55 | public function checkPaymentStatus(): void
56 | {
57 | // 刷新订单状态
58 | $this->orderData->refresh();
59 |
60 | // 检查订单是否已完成
61 | if ($this->orderData->status == Order::STATUS_COMPLETED) {
62 | $this->paymentCompleted = true;
63 | // 延迟跳转,让用户看到成功提示
64 | $this->dispatch('payment-completed');
65 | }
66 |
67 | // 检查订单是否过期
68 | if ($this->orderData->status == Order::STATUS_EXPIRED) {
69 | session()->flash('error', '订单已过期');
70 | $this->redirect(route('search-order'));
71 | }
72 | }
73 |
74 | public function render(SeoService $seoService)
75 | {
76 | $seoData = $seoService->getDefaultSeoData('扫码支付', true);
77 |
78 | return view('livewire.pages.qr-pay')->with($seoData);
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/app/Filament/Resources/Emailtpls/Tables/EmailtplsTable.php:
--------------------------------------------------------------------------------
1 | columns([
26 | TextColumn::make('id')
27 | ->label('ID')
28 | ->sortable(),
29 |
30 | TextColumn::make('tpl_name')
31 | ->label('模板名称')
32 | ->searchable()
33 | ->sortable(),
34 |
35 | TextColumn::make('tpl_token')
36 | ->label('模板标识')
37 | ->searchable()
38 | ->copyable()
39 | ->badge()
40 | ->color('info'),
41 |
42 | TextColumn::make('tpl_subject')
43 | ->label('邮件主题')
44 | ->limit(40)
45 | ->searchable(),
46 |
47 | TextColumn::make('created_at')
48 | ->label('创建时间')
49 | ->dateTime('Y-m-d H:i')
50 | ->sortable()
51 | ->toggleable(),
52 |
53 | TextColumn::make('updated_at')
54 | ->label('更新时间')
55 | ->dateTime('Y-m-d H:i')
56 | ->sortable()
57 | ->toggleable(isToggledHiddenByDefault: true),
58 | ])
59 | ->filters([
60 | TrashedFilter::make(),
61 | ])
62 | ->recordActions([
63 | ViewAction::make(),
64 | EditAction::make(),
65 | ])
66 | ->toolbarActions([
67 | BulkActionGroup::make([
68 | DeleteBulkAction::make(),
69 | ForceDeleteBulkAction::make(),
70 | RestoreBulkAction::make(),
71 | ]),
72 | ])
73 | ->defaultSort('id', 'desc');
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/database/migrations/2025_11_01_013557_create_orders_table.php:
--------------------------------------------------------------------------------
1 | id();
20 | $table->string('order_sn', 150)->comment('订单号');
21 | $table->unsignedInteger('goods_id')->comment('关联商品id');
22 | $table->unsignedInteger('coupon_id')->default(0)->comment('关联优惠码id');
23 | $table->string('title', 200)->comment('订单名称');
24 | $table->tinyInteger('type')->default(1)->comment('1自动发货 2人工处理');
25 | $table->decimal('goods_price', 10, 2)->default(0)->comment('商品单价');
26 | $table->integer('buy_amount')->default(1)->comment('购买数量');
27 | $table->decimal('coupon_discount_price', 10, 2)->default(0)->comment('优惠码优惠价格');
28 | $table->decimal('wholesale_discount_price', 10, 2)->default(0)->comment('批发价优惠');
29 | $table->decimal('total_price', 10, 2)->default(0)->comment('订单总价');
30 | $table->decimal('actual_price', 10, 2)->default(0)->comment('实际支付价格');
31 | $table->string('search_pwd', 200)->default('')->comment('查询密码');
32 | $table->string('email', 200)->comment('下单邮箱');
33 | $table->text('info')->nullable()->comment('订单详情');
34 | $table->unsignedInteger('pay_id')->nullable()->comment('支付通道id');
35 | $table->string('buy_ip', 50)->comment('购买者下单IP地址');
36 | $table->string('trade_no', 200)->default('')->comment('第三方支付订单号');
37 | $table->tinyInteger('status')->default(1)->comment('1待支付 2待处理 3处理中 4已完成 5处理失败 6异常 -1过期');
38 | $table->boolean('coupon_ret_back')->default(false)->comment('优惠码使用次数是否已经回退 0否 1是');
39 | $table->timestamps();
40 | $table->softDeletes();
41 |
42 | $table->unique('order_sn', 'idx_order_sn');
43 | $table->index('goods_id', 'idx_orders_goods_id');
44 | $table->index('email', 'idex_email');
45 | });
46 | }
47 |
48 | /**
49 | * Reverse the migrations.
50 | */
51 | public function down(): void
52 | {
53 | Schema::dropIfExists('orders');
54 | }
55 | };
56 |
--------------------------------------------------------------------------------
/app/Filament/Resources/Emailtpls/Schemas/EmailtplForm.php:
--------------------------------------------------------------------------------
1 | columns(1)
21 | ->components([
22 | Section::make('邮件模板信息')
23 | ->columnSpan('full')
24 | ->schema([
25 | TextInput::make('tpl_name')
26 | ->label('模板名称')
27 | ->required()
28 | ->maxLength(255),
29 |
30 | TextInput::make('tpl_token')
31 | ->label('模板标识')
32 | ->required()
33 | ->unique(ignoreRecord: true)
34 | ->maxLength(255)
35 | ->helperText('唯一标识,如: order_paid')
36 | ->disabled(fn ($context) => $context === 'edit'),
37 |
38 | TextInput::make('tpl_subject')
39 | ->label('邮件主题')
40 | ->required()
41 | ->maxLength(255),
42 |
43 | RichEditor::make('tpl_content')
44 | ->label('邮件内容')
45 | ->required()
46 | ->toolbarButtons([
47 | 'bold',
48 | 'italic',
49 | 'underline',
50 | 'strike',
51 | 'link',
52 | 'h2',
53 | 'h3',
54 | 'bulletList',
55 | 'orderedList',
56 | 'blockquote',
57 | 'codeBlock',
58 | 'undo',
59 | 'redo',
60 | ])
61 | ->helperText('支持变量替换,如 {order_sn}, {title}')
62 | ->columnSpanFull(),
63 | ]),
64 | ]);
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/app/Filament/Resources/Carmis/CarmisResource.php:
--------------------------------------------------------------------------------
1 | ListCarmis::route('/'),
61 | 'create' => CreateCarmis::route('/create'),
62 | 'edit' => EditCarmis::route('/{record}/edit'),
63 | ];
64 | }
65 |
66 | public static function getRecordRouteBindingEloquentQuery(): Builder
67 | {
68 | return parent::getRecordRouteBindingEloquentQuery()
69 | ->withoutGlobalScopes([
70 | SoftDeletingScope::class,
71 | ]);
72 | }
73 |
74 | public static function getNavigationBadge(): ?string
75 | {
76 | return (string) static::getModel()::where('status', Carmis::STATUS_UNSOLD)->count();
77 | }
78 |
79 | public static function getNavigationBadgeColor(): ?string
80 | {
81 | return 'success';
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/app/Filament/Resources/Orders/Pages/ListOrders.php:
--------------------------------------------------------------------------------
1 | Tab::make('全部')
37 | ->badge(Order::count()),
38 | 'wait_pay' => Tab::make('待支付')
39 | ->badge(Order::where('status', Order::STATUS_WAIT_PAY)->count())
40 | ->badgeColor('warning')
41 | ->modifyQueryUsing(fn (Builder $query) => $query->where('status', Order::STATUS_WAIT_PAY)),
42 | 'pending' => Tab::make('待处理')
43 | ->badge(Order::where('status', Order::STATUS_PENDING)->count())
44 | ->badgeColor('info')
45 | ->modifyQueryUsing(fn (Builder $query) => $query->where('status', Order::STATUS_PENDING)),
46 | 'processing' => Tab::make('处理中')
47 | ->badge(Order::where('status', Order::STATUS_PROCESSING)->count())
48 | ->badgeColor('primary')
49 | ->modifyQueryUsing(fn (Builder $query) => $query->where('status', Order::STATUS_PROCESSING)),
50 | 'completed' => Tab::make('已完成')
51 | ->badge(Order::where('status', Order::STATUS_COMPLETED)->count())
52 | ->badgeColor('success')
53 | ->modifyQueryUsing(fn (Builder $query) => $query->where('status', Order::STATUS_COMPLETED)),
54 | 'failed' => Tab::make('失败/取消')
55 | ->badge(Order::whereIn('status', [Order::STATUS_FAILURE, Order::STATUS_EXPIRED, Order::STATUS_ABNORMAL])->count())
56 | ->badgeColor('danger')
57 | ->modifyQueryUsing(fn (Builder $query) => $query->whereIn('status', [Order::STATUS_FAILURE, Order::STATUS_EXPIRED, Order::STATUS_ABNORMAL])),
58 | ];
59 | }
60 | }
61 |
--------------------------------------------------------------------------------