Browse Source

feat: brand new coupon system for the new shop

Cat 2 years ago
parent
commit
d6c60d011e

+ 5 - 4
app/routes.php

@@ -224,9 +224,10 @@ return function (SlimApp $app): void {
         $this->post('/user/ajax', App\Controllers\Admin\UserController::class . ':ajax');
 
         // Coupon Mange
-        $this->get('/coupon', App\Controllers\AdminController::class . ':coupon');
-        $this->post('/coupon', App\Controllers\AdminController::class . ':addCoupon');
-        $this->post('/coupon/ajax', App\Controllers\AdminController::class . ':ajaxCoupon');
+        $this->get('/coupon', App\Controllers\Admin\CouponController::class . ':index');
+        $this->post('/coupon', App\Controllers\Admin\CouponController::class . ':add');
+        $this->post('/coupon/ajax', App\Controllers\Admin\CouponController::class . ':ajax');
+        $this->delete('/coupon/{id}', App\Controllers\Admin\CouponController::class . ':delete');
 
         // Subscribe Log Mange
         $this->get('/subscribe', App\Controllers\Admin\SubscribeLogController::class . ':index');
@@ -272,7 +273,7 @@ return function (SlimApp $app): void {
         // 账单
         $this->get('/invoice', App\Controllers\Admin\InvoiceController::class . ':index');
         $this->get('/invoice/{id}/view', App\Controllers\Admin\InvoiceController::class . ':detail');
-        $this->post('/invoice/{id}/mark_paid', App\Controllers\Admin\InvoiceController::class . ':mark_paid');
+        $this->post('/invoice/{id}/mark_paid', App\Controllers\Admin\InvoiceController::class . ':markPaid');
         $this->post('/invoice/ajax', App\Controllers\Admin\InvoiceController::class . ':ajax');
     })->add(new Admin());
 

+ 12 - 0
db/migrations/20000101000000_init_database.php.new

@@ -384,6 +384,18 @@ final class InitDatabase extends AbstractMigration
             ->addIndex([ 'order_id' ])
             ->addIndex([ 'status' ])
             ->create();
+
+        $this->table('user_coupon', [ 'id' => false, 'primary_key' => [ 'id' ]])
+            ->addColumn('id', 'integer', [ 'comment' => '优惠码ID', 'identity' => true ])
+            ->addColumn('code', 'string', [ 'comment' => '优惠码' ])
+            ->addColumn('content', 'json', [ 'comment' => '优惠码内容' ])
+            ->addColumn('limit', 'json', [ 'comment' => '优惠码限制' ])
+            ->addColumn('create_time', 'integer', [ 'comment' => '创建时间' ])
+            ->addColumn('expire_time', 'integer', [ 'comment' => '过期时间' ])
+            ->addIndex([ 'id' ])
+            ->addIndex([ 'code' ])
+            ->addIndex([ 'expire_time' ])
+            ->create();
     }
 
     public function down(): void

+ 30 - 0
db/migrations/20230115090200_add_user_coupon.php

@@ -0,0 +1,30 @@
+<?php
+
+declare(strict_types=1);
+
+use Phinx\Migration\AbstractMigration;
+
+final class AddUserCoupon extends AbstractMigration
+{
+    public function up(): void
+    {
+        if (! $this->hasTable('user_coupon')) {
+            $this->table('user_coupon', [ 'id' => false, 'primary_key' => [ 'id' ]])
+                ->addColumn('id', 'integer', [ 'comment' => '优惠码ID', 'identity' => true ])
+                ->addColumn('code', 'string', [ 'comment' => '优惠码' ])
+                ->addColumn('content', 'json', [ 'comment' => '优惠码内容' ])
+                ->addColumn('limit', 'json', [ 'comment' => '优惠码限制' ])
+                ->addColumn('create_time', 'integer', [ 'comment' => '创建时间' ])
+                ->addColumn('expire_time', 'integer', [ 'comment' => '过期时间' ])
+                ->addIndex([ 'id' ])
+                ->addIndex([ 'code' ])
+                ->addIndex([ 'expire_time' ])
+                ->create();
+        }
+    }
+
+    public function down(): void
+    {
+        $this->table('user_coupon')->drop()->update();
+    }
+}

+ 204 - 92
resources/views/tabler/admin/coupon.tpl

@@ -1,116 +1,228 @@
-{include file='admin/main.tpl'}
+{include file='admin/tabler_header.tpl'}
 
-<main class="content">
-    <div class="content-header ui-content-header">
-        <div class="container">
-            <h1 class="content-heading">优惠码</h1>
+<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
+<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
+
+<div class="page-wrapper">
+    <div class="container-xl">
+        <div class="page-header d-print-none text-white">
+            <div class="row align-items-center">
+                <div class="col">
+                    <h2 class="page-title">
+                        <span class="home-title">优惠码</span>
+                    </h2>
+                    <div class="page-pretitle my-3">
+                        <span class="home-subtitle">
+                            查看并管理优惠码
+                        </span>
+                    </div>
+                </div>
+                <div class="col-auto ms-auto d-print-none">
+                    <div class="btn-list">
+                        <a href="#" class="btn btn-primary d-none d-sm-inline-block" data-bs-toggle="modal"
+                            data-bs-target="#create-dialog">
+                            <i class="icon ti ti-plus"></i>
+                            创建
+                        </a>
+                        <a href="#" class="btn btn-primary d-sm-none btn-icon" data-bs-toggle="modal"
+                            data-bs-target="#create-dialog">
+                            <i class="icon ti ti-plus"></i>
+                        </a>
+                    </div>
+                </div>
+            </div>
         </div>
     </div>
-    <div class="container">
-        <section class="content-inner margin-top-no">
-            <div class="card">
-                <div class="card-main">
-                    <div class="card-inner">
-                        <div class="form-group form-group-label">
-                            <label class="floating-label" for="prefix">优惠码</label>
-                            <input class="form-control maxwidth-edit" id="prefix" type="text">
-                            <p class="form-control-guide"><i class="mdi mdi-information"></i>生成随机优惠码不填</p>
-                        </div>
-                        <div class="form-group form-group-label">
-                            <label class="floating-label" for="credit">优惠码额度</label>
-                            <input class="form-control maxwidth-edit" id="credit" type="text">
-                            <p class="form-control-guide"><i class="mdi mdi-information"></i>百分比,九折就填 10</p>
-                        </div>
-                        <div class="form-group form-group-label">
-                            <label class="floating-label" for="expire">优惠码有效期(h)</label>
-                            <input class="form-control maxwidth-edit" id="expire" type="number" value="1">
-                        </div>
-                        <div class="form-group form-group-label">
-                            <label class="floating-label" for="shop">优惠码可用商品ID</label>
-                            <input class="form-control maxwidth-edit" id="shop" type="text">
-                            <p class="form-control-guide"><i class="mdi mdi-information"></i>不填即为所有商品可用,多个的话用英文半角逗号分割</p>
-                        </div>
-                        <div class="form-group form-group-label">
-                            <label class="floating-label" for="shop">优惠码每个用户可用次数,-1为无限次</label>
-                            <input class="form-control maxwidth-edit" id="count" type="number" value="1">
-                        </div>
-                        <div class="form-group form-group-label">
-                            <label for="generate-type">
-                                <label class="floating-label" for="sort">选择生成方式</label>
-                                <select id="generate-type" class="form-control maxwidth-edit">
-                                    <option value="1">指定字符</option>
-                                    <option value="2">随机字符</option>
-                                    <option value="3">指定字符+随机字符</option>
-                                </select>
-                            </label>
-                        </div>
-                        <div class="form-group">
-                            <div class="row">
-                                <div class="col-md-10 col-md-push-1">
-                                    <button id="coupon" type="submit"
-                                            class="btn btn-block btn-brand waves-attach waves-light">生成优惠码
-                                    </button>
-                                </div>
-                            </div>
+    <div class="page-body">
+        <div class="container-xl">
+            <div class="row row-deck row-cards">
+                <div class="col-12">
+                    <div class="card">
+                        <div class="table-responsive">
+                            <table id="data_table" class="table card-table table-vcenter text-nowrap datatable">
+                                <thead>
+                                    <tr>
+                                        {foreach $details['field'] as $key => $value}
+                                            <th>{$value}</th>
+                                        {/foreach}
+                                    </tr>
+                                </thead>
+                            </table>
                         </div>
                     </div>
                 </div>
             </div>
-            <div class="card margin-bottom-no">
-                <div class="card-main">
-                    <div class="card-inner">
-                        <p class="card-heading">优惠码</p>
-                        <p>显示表项:
-                            {include file='table/checkbox.tpl'}
-                        </p>
-                        <div class="card-table">
-                            <div class="table-responsive">
-                                {include file='table/table.tpl'}
+        </div>
+    </div>
+
+    <div class="modal modal-blur fade" id="create-dialog" tabindex="-1" role="dialog" aria-hidden="true">
+        <div class="modal-dialog modal-dialog-centered modal-dialog-scrollable" role="document">
+            <div class="modal-content">
+                <div class="modal-header">
+                    <h5 class="modal-title">优惠码内容</h5>
+                    <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
+                </div>
+                <div class="modal-body">
+                    {foreach $details['create_dialog'] as $detail}
+                        {if $detail['type'] == 'input'}
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">{$detail['info']}</label>
+                                <div class="col">
+                                    <input id="{$detail['id']}" type="text" class="form-control"
+                                        placeholder="{$detail['placeholder']}">
+                                </div>
+                            </div>
+                        {/if}
+                        {if $detail['type'] == 'textarea'}
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">{$detail['info']}</label>
+                                <textarea id="{$detail['id']}" class="col form-control" rows="{$detail['rows']}"
+                                    placeholder="{$detail['placeholder']}"></textarea>
                             </div>
+                        {/if}
+                        {if $detail['type'] == 'select'}
+                            <div class="form-group mb-3 row">
+                                <label class="form-label col-3 col-form-label">{$detail['info']}</label>
+                                <div class="col">
+                                    <select id="{$detail['id']}" class="col form-select">
+                                        {foreach $detail['select'] as $key => $value}
+                                            <option value="{$key}">{$value}</option>
+                                        {/foreach}
+                                    </select>
+                                </div>
+                            </div>
+                        {/if}
+                    {/foreach}
+                    <div class="form-group mb-3 row">
+                        <label class="form-label col-3 col-form-label">过期时间</label>
+                        <div class="col">
+                            <input id="expire_time" type="text" class="form-control"
+                                placeholder="">
                         </div>
                     </div>
                 </div>
+                <div class="modal-footer">
+                    <button type="button" class="btn me-auto" data-bs-dismiss="modal">取消</button>
+                    <button id="create-button" onclick="createGiftCard()"
+                        type="button" class="btn btn-primary" data-bs-dismiss="modal">创建</button>
+                </div>
             </div>
-            {include file='dialog.tpl'}
-        </section>
+        </div>
     </div>
-</main>
 
-{include file='admin/footer.tpl'}
+    <script>
+        flatpickr("#expire_time", {
+            enableTime: true,
+            dateFormat: "U",
+            time_24hr: true,
+            minDate: "today",
+            locale: "zh"
+        });
 
-<script>
-    {include file='table/js_1.tpl'}
-    window.addEventListener('load', () => {
-        {include file='table/js_2.tpl'}
+        var table = $('#data_table').DataTable({
+            ajax: {
+                url: '/admin/coupon/ajax',
+                type: 'POST',
+                dataSrc: 'coupons'
+            },
+            "autoWidth":false,
+            'iDisplayLength': 10,
+            'scrollX': true,
+            'order': [
+                [1, 'desc']
+            ],
+            columns: [
+                {foreach $details['field'] as $key => $value}
+                { data: '{$key}' },
+                {/foreach}
+            ],
+            "columnDefs":[
+                { targets:[0],orderable:false }
+            ],
+            "dom": "<'row px-3 py-3'<'col-sm-12 col-md-6'l><'col-sm-12 col-md-6'f>>" +
+                "<'row'<'col-sm-12'tr>>" +
+                "<'row card-footer d-flex d-flexalign-items-center'<'col-sm-12 col-md-5'i><'col-sm-12 col-md-7'p>>",
+            language: {
+                "sProcessing": "处理中...",
+                "sLengthMenu": "显示 _MENU_ 条",
+                "sZeroRecords": "没有匹配结果",
+                "sInfo": "第 _START_ 至 _END_ 项结果,共 _TOTAL_项",
+                "sInfoEmpty": "第 0 至 0 项结果,共 0 项",
+                "sInfoFiltered": "(在 _MAX_ 项中查找)",
+                "sInfoPostFix": "",
+                "sSearch": "<i class=\"ti ti-search\"></i> ",
+                "sUrl": "",
+                "sEmptyTable": "表中数据为空",
+                "sLoadingRecords": "载入中...",
+                "sInfoThousands": ",",
+                "oPaginate": {
+                    "sFirst": "首页",
+                    "sPrevious": "<i class=\"titi-arrow-left\"></i>",
+                    "sNext": "<i class=\"ti ti-arrow-right\"><i>",
+                    "sLast": "末页"
+                },
+                "oAria": {
+                    "sSortAscending": ": 以升序排列此列",
+                    "sSortDescending": ": 以降序排列此列"
+                }
+            },
+        });
 
-        $$.getElementById('coupon').addEventListener('click', () => {
-            let couponCode = $$getValue('prefix');
+        function loadTable() {
+            table;
+        }
 
+        function createCoupon() {
             $.ajax({
-                type: "POST",
-                url: "/admin/coupon",
+                url: '/admin/coupon',
+                type: 'POST',
                 dataType: "json",
                 data: {
-                    prefix: $$getValue('prefix'),
-                    credit: $$getValue('credit'),
-                    shop: $$getValue('shop'),
-                    onetime: $$getValue('count'),
-                    expire: $$getValue('expire'),
-                    generate_type: $$getValue('generate-type'),
+                    {foreach $details['create_dialog'] as $detail}
+                        {$detail['id']}: $('#{$detail['id']}').val(),
+                    {/foreach}
+                    expire_time: $('#expire_time').val(),
                 },
-                success: data => {
-                    $("#result").modal();
-                    $$.getElementById('msg').innerHTML = data.msg;
-                    if (data.ret) {
-                        window.setTimeout("location.href='/admin/coupon'", {$config['jump_delay']});
+                success: function(data) {
+                    if (data.ret == 1) {
+                        $('#success-message').text(data.msg);
+                        $('#success-dialog').modal('show');
+                        reloadTableAjax();
+                    } else {
+                        $('#fail-message').text(data.msg);
+                        $('#fail-dialog').modal('show');
                     }
-                },
-                error: jqXHR => {
-                    alert(`发生错误:${
-                            jqXHR.status
-                            }`);
                 }
             })
-        })
-    })
-</script>
+        };
+
+        function deleteCoupon(coupon_id) {
+            $('#notice-message').text('确定删除此优惠码?');
+            $('#notice-dialog').modal('show');
+            $('#notice-confirm').on('click', function() {
+                $.ajax({
+                    url: "/admin/coupon/" + coupon_id,
+                    type: 'DELETE',
+                    dataType: "json",
+                    success: function(data) {
+                        if (data.ret == 1) {
+                            $('#success-message').text(data.msg);
+                            $('#success-dialog').modal('show');
+                            reloadTableAjax();
+                        } else {
+                            $('#fail-message').text(data.msg);
+                            $('#fail-dialog').modal('show');
+                        }
+                    }
+                })
+            });
+        };
+
+        function reloadTableAjax() {
+            table.ajax.reload(null, false);
+        }
+
+        loadTable();
+    </script>
+
+{include file='admin/tabler_footer.tpl'}

+ 234 - 0
src/Controllers/Admin/CouponController.php

@@ -0,0 +1,234 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Controllers\Admin;
+
+use App\Controllers\BaseController;
+use App\Models\UserCoupon;
+use App\Utils\Tools;
+use Psr\Http\Message\ResponseInterface;
+use Slim\Http\Request;
+use Slim\Http\Response;
+
+/*
+ *  Coupon Controller
+ */
+final class CouponController extends BaseController
+{
+    public static $details = [
+        'field' => [
+            'op' => '操作',
+            'id' => '优惠码ID',
+            'code' => '优惠码',
+            'type' => '优惠码类型',
+            'value' => '优惠码额度',
+            'product_id' => '可用商品ID',
+            'use_time' => '每个用户可使用次数',
+            'new_user' => '仅限新用户使用',
+            'create_time' => '创建时间',
+            'expire_time' => '过期时间',
+        ],
+        'create_dialog' => [
+            [
+                'id' => 'code',
+                'info' => '优惠码',
+                'type' => 'input',
+                'placeholder' => '',
+            ],
+            [
+                'id' => 'type',
+                'info' => '优惠码类型',
+                'type' => 'select',
+                'select' => [
+                    'percentage' => '百分比',
+                    'fixed' => '固定金额',
+                ],
+            ],
+            [
+                'id' => 'value',
+                'info' => '优惠码额度',
+                'type' => 'input',
+                'placeholder' => '',
+            ],
+            [
+                'id' => 'product_id',
+                'info' => '可用商品ID',
+                'type' => 'input',
+                'placeholder' => '',
+            ],
+            [
+                'id' => 'use_time',
+                'info' => '每个用户可使用次数',
+                'type' => 'input',
+                'placeholder' => '',
+            ],
+            [
+                'id' => 'new_user',
+                'info' => '仅限新用户使用',
+                'type' => 'input',
+                'placeholder' => '',
+            ],
+            [
+                'id' => 'generate_method',
+                'info' => '生成方式',
+                'type' => 'select',
+                'select' => [
+                    'char' => '指定字符',
+                    'random' => '随机字符(无视优惠码参数)',
+                    'char_ramdom' => '指定字符+随机字符',
+                ],
+            ],
+        ],
+    ];
+
+    /**
+     * 后台优惠码页面
+     *
+     * @param array     $args
+     */
+    public function index(Request $request, Response $response, array $args): ResponseInterface
+    {
+        return $response->write(
+            $this->view()
+                ->assign('details', self::$details)
+                ->display('admin/coupon.tpl')
+        );
+    }
+
+    /**
+     * 添加优惠码
+     *
+     * @param array     $args
+     */
+    public function add(Request $request, Response $response, array $args): ResponseInterface
+    {
+        $code = $request->getParam('code');
+        $type = $request->getParam('type');
+        $value = $request->getParam('value');
+        $product_id = $request->getParam('product_id');
+        $use_time = $request->getParam('use_time');
+        $new_user = $request->getParam('new_user');
+        $generate_method = $request->getParam('generate_method');
+        $expire_time = $request->getParam('expire_time');
+
+        if ($code === '' && \in_array($generate_method, ['char', 'char_ramdom'])) {
+            return $response->withJson([
+                'ret' => 0,
+                'msg' => '优惠码不能为空',
+            ]);
+        }
+
+        if ($type === '') {
+            return $response->withJson([
+                'ret' => 0,
+                'msg' => '优惠码类型不能为空',
+            ]);
+        }
+
+        if ($value === '') {
+            return $response->withJson([
+                'ret' => 0,
+                'msg' => '优惠码额度不能为空',
+            ]);
+        }
+
+        if ($expire_time < \time()) {
+            return $response->withJson([
+                'ret' => 0,
+                'msg' => '到期时间不能小于当前时间',
+            ]);
+        }
+
+        if ($generate_method === 'char') {
+            if (UserCoupon::where('code', $code)->count() !== 0) {
+                return $response->withJson([
+                    'ret' => 0,
+                    'msg' => '优惠码已存在',
+                ]);
+            }
+        }
+
+        if ($generate_method === 'char_ramdom') {
+            $code .= Tools::genRandomChar(8);
+
+            if (UserCoupon::where('code', $code)->count() === 0) {
+                return $response->withJson([
+                    'ret' => 0,
+                    'msg' => '出现了一些问题,请稍后重试',
+                ]);
+            }
+        }
+
+        if ($generate_method === 'ramdom') {
+            $code = Tools::genRandomChar(8);
+
+            if (UserCoupon::where('code', $code)->count() === 0) {
+                return $response->withJson([
+                    'ret' => 0,
+                    'msg' => '出现了一些问题,请稍后重试',
+                ]);
+            }
+        }
+
+        $content = [
+            'type' => $type,
+            'value' => $value,
+        ];
+
+        $limit = [
+            'product_id' => $product_id,
+            'use_time' => $use_time,
+            'new_user' => $new_user,
+        ];
+
+        $coupon = new UserCoupon();
+        $coupon->code = $code;
+        $coupon->content = \json_encode($content);
+        $coupon->limit = \json_encode($limit);
+        $coupon->create_time = \time();
+        $coupon->expire_time = $expire_time;
+        $coupon->save();
+
+        return $response->withJson([
+            'ret' => 1,
+            'msg' => '优惠码 <code>' . $code . '</code> 添加成功',
+        ]);
+    }
+
+    public function delete(Request $request, Response $response, array $args): ResponseInterface
+    {
+        $coupon_id = $args['id'];
+        UserCoupon::find($coupon_id)->delete();
+        return $response->withJson([
+            'ret' => 1,
+            'msg' => '删除成功',
+        ]);
+    }
+
+    /**
+     * 后台商品优惠码页面 AJAX
+     *
+     * @param array     $args
+     */
+    public function ajax(Request $request, Response $response, array $args): ResponseInterface
+    {
+        $coupons = UserCoupon::orderBy('id', 'desc')->get();
+        foreach ($coupons as $coupon) {
+            $content = \json_decode($coupon->content);
+            $limit = \json_decode($coupon->limit);
+            $coupon->op = '<button type="button" class="btn btn-red" id="delete-coupon-' . $coupon->id . '" 
+        onclick="deleteCoupons(' . $coupon->id . ')">删除</button>';
+            $coupon->type = Tools::getCouponType($content);
+            $coupon->value = $content->value;
+            $coupon->product_id = $limit->product_id;
+            $coupon->use_time = $limit->use_time;
+            $coupon->new_user = $limit->new_user;
+            $coupon->create_time = Tools::toDateTime((int) $coupon->create_time);
+            $coupon->expire_time = Tools::toDateTime((int) $coupon->expire_time);
+        }
+        return $response->withJson([
+            'coupons' => $coupons,
+        ]);
+    }
+}

+ 16 - 13
src/Controllers/Admin/GiftCardController.php

@@ -7,6 +7,9 @@ namespace App\Controllers\Admin;
 use App\Controllers\BaseController;
 use App\Models\GiftCard;
 use App\Utils\Tools;
+use Psr\Http\Message\ResponseInterface;
+use Slim\Http\Request;
+use Slim\Http\Response;
 
 final class GiftCardController extends BaseController
 {
@@ -49,7 +52,7 @@ final class GiftCardController extends BaseController
         ],
     ];
 
-    public function index($request, $response, $args)
+    public function index(Request $request, Response $response, array $args): ResponseInterface
     {
         return $response->write(
             $this->view()
@@ -58,7 +61,7 @@ final class GiftCardController extends BaseController
         );
     }
 
-    public function add($request, $response, $args)
+    public function add(Request $request, Response $response, array $args): ResponseInterface
     {
         $card_number = $request->getParam('card_number');
         $card_value = $request->getParam('card_value');
@@ -99,7 +102,17 @@ final class GiftCardController extends BaseController
         ]);
     }
 
-    public function ajax($request, $response, $args)
+    public function delete(Request $request, Response $response, array $args): ResponseInterface
+    {
+        $card_id = $args['id'];
+        GiftCard::find($card_id)->delete();
+        return $response->withJson([
+            'ret' => 1,
+            'msg' => '删除成功',
+        ]);
+    }
+
+    public function ajax(Request $request, Response $response, array $args): ResponseInterface
     {
         $giftcards = GiftCard::orderBy('id', 'desc')->get();
         foreach ($giftcards as $giftcard) {
@@ -113,14 +126,4 @@ final class GiftCardController extends BaseController
             'giftcards' => $giftcards,
         ]);
     }
-
-    public function delete($request, $response, $args)
-    {
-        $card_id = $args['id'];
-        GiftCard::find($card_id)->delete();
-        return $response->withJson([
-            'ret' => 1,
-            'msg' => '删除成功',
-        ]);
-    }
 }

+ 2 - 2
src/Controllers/Admin/InvoiceController.php

@@ -56,12 +56,12 @@ final class InvoiceController extends BaseController
         );
     }
 
-    public function mark_paid(Request $request, Response $response, array $args): ResponseInterface
+    public function markPaid(Request $request, Response $response, array $args): ResponseInterface
     {
         $invoice_id = $args['id'];
         $invoice = Invoice::find($invoice_id);
 
-        if ($invoice->status === 'paid_gateway' || $invoice->status === 'paid_balance' || $invoice->status === 'paid_admin') {
+        if (\in_array($invoice->status, ['paid_gateway', 'paid_balance', 'paid_admin', 'paid_giftcard'])) {
             return $response->withJson([
                 'ret' => 0,
                 'msg' => '不能标记已经支付的账单',

+ 9 - 1
src/Controllers/Admin/OrderController.php

@@ -85,9 +85,17 @@ final class OrderController extends BaseController
         $order->save();
 
         $invoice = Invoice::where('order_id', $order_id)->first();
+
+        if ($invoice === null) {
+            return $response->withJson([
+                'ret' => 1,
+                'msg' => '订单取消成功,但关联账单状态异常',
+            ]);
+        }
+
         $invoice->update_time = \time();
 
-        if ($invoice->status === 'paid_gateway' || $invoice->status === 'paid_balance' || $invoice->status === 'paid_admin') {
+        if (\in_array($invoice->status, ['paid_gateway', 'paid_balance', 'paid_admin', 'paid_giftcard'])) {
             $invoice->status = 'cancelled';
             $invoice->save();
 

+ 11 - 0
src/Models/UserCoupon.php

@@ -0,0 +1,11 @@
+<?php
+
+declare(strict_types=1);
+
+namespace App\Models;
+
+final class UserCoupon extends Model
+{
+    protected $connection = 'default';
+    protected $table = 'user_coupon';
+}

+ 28 - 0
src/Utils/Tools.php

@@ -602,6 +602,9 @@ final class Tools
         if ($invoice->status === 'paid_balance') {
             return '已支付(账户余额)';
         }
+        if ($invoice->status === 'paid_giftcard') {
+            return '已支付(礼品卡)';
+        }
         if ($invoice->status === 'paid_admin') {
             return '已支付(管理员)';
         }
@@ -610,4 +613,29 @@ final class Tools
         }
         return '未知';
     }
+
+    /**
+     * 优惠码状态
+     */
+    public static function getCouponStatus($coupon)
+    {
+        if ($coupon->expire_time < \time()) {
+            return '已过期';
+        }
+        return '激活';
+    }
+
+    /**
+     * 优惠码类型
+     */
+    public static function getCouponType($content)
+    {
+        if ($content->type === 'percentage') {
+            return '百分比';
+        }
+        if ($content->type === 'fixed') {
+            return '固定金额';
+        }
+        return '未知';
+    }
 }