├── 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 |
3 | {{ $this->schema }} 4 | 5 |
6 | @foreach ($this->getFormActions() as $action) 7 | {{ $action }} 8 | @endforeach 9 |
10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/Filament/Resources/GoodsGroups/Pages/CreateGoodsGroup.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{ $this->schema }} 4 | 5 |
6 | @foreach ($this->getFormActions() as $action) 7 | {{ $action }} 8 | @endforeach 9 |
10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /resources/views/filament/pages/system-setting.blade.php: -------------------------------------------------------------------------------- 1 | 2 |
3 | {{ $this->schema }} 4 | 5 |
6 | @foreach ($this->getFormActions() as $action) 7 | {{ $action }} 8 | @endforeach 9 |
10 |
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 | --------------------------------------------------------------------------------