罗永浩 пре 7 месеци
родитељ
комит
fef9f19d26

+ 161 - 0
addons/admin/src/views/dashboard/components/AnswerRanks.vue

@@ -0,0 +1,161 @@
+<template>
+    <div class="mt-10">
+        <!-- 筛选表单 -->
+        <el-form :model="query" inline label-width="100px" size="small" @submit.native.prevent>
+            <el-form-item label="时间类型">
+                <el-select v-model="query.dateType" placeholder="请选择">
+                    <el-option label="日" value="day" />
+                    <el-option label="周" value="week" />
+                    <el-option label="月" value="month" />
+                </el-select>
+            </el-form-item>
+
+            <el-form-item label="开始时间">
+                <el-date-picker v-model="query.start_time" :type="datePickerType" placeholder="选择开始时间"
+                    value-format="yyyy-MM-dd" />
+            </el-form-item>
+
+            <el-form-item label="结束时间">
+                <el-date-picker v-model="query.end_time" :type="datePickerType" placeholder="选择结束时间"
+                    value-format="yyyy-MM-dd" />
+            </el-form-item>
+
+            <el-form-item label="状态">
+                <el-select v-model="query.status" placeholder="选择状态" clearable>
+                    <el-option label="已完成" :value="1" />
+                    <el-option label="待处理" :value="2" />
+                    <el-option label="失败/取消" :value="3" />
+                </el-select>
+            </el-form-item>
+
+            <el-form-item>
+                <el-button type="primary" @click="onSearch">查询</el-button>
+                <el-button @click="resetQuery">重置</el-button>
+            </el-form-item>
+        </el-form>
+
+        <!-- 表格 -->
+        <el-table :data="list" border style="width: 100%; margin-top: 10px;">
+            <el-table-column prop="stat_date" :label="dateLabel" />
+            <el-table-column prop="total_count" label="答题数量" />
+            <el-table-column prop="total_time" label="答题时间">
+                <template #default="{ row }">{{ formatSeconds(row.total_time) }}</template>
+            </el-table-column>
+        </el-table>
+
+        <!-- 分页 -->
+        <el-pagination v-model:current-page="query.page" v-model:page-size="query.limit"
+            layout="total, sizes, prev, pager, next, jumper" :total="total" :page-sizes="[10, 20, 50, 100]"
+            style="margin-top: 10px; text-align: right;" @current-change="onPageChange" @size-change="onSizeChange" />
+    </div>
+</template>
+
+<script>
+export default {
+    name: "AnswerRanks",
+    data() {
+        return {
+            query: {
+                dateType: "day",
+                start_time: "",
+                end_time: "",
+                type: "",
+                status: "",
+                page: 1,
+                limit: 10,
+            },
+            list: [],
+            total: 0
+        };
+    },
+    watch: {
+        query: {
+            handler() {
+                this.query.page = 1; // 筛选条件变化回到第一页
+                this.loadList();
+            },
+            deep: true, // 深度监听对象内部字段
+        },
+    },
+    computed: {
+        dateLabel() {
+            switch (this.query.dateType) {
+                case "day":
+                    return "日期";
+                case "week":
+                    return "周数";
+                case "month":
+                    return "月份";
+                case "year":
+                    return "年份";
+                default:
+                    return "时间";
+            }
+        },
+        datePickerType() {
+            switch (this.query.dateType) {
+                case "year":
+                    return "year";
+                case "week":
+                    return "week";
+                case "month":
+                    return "month";
+                default:
+                    return "date";
+            }
+        }
+    },
+    created() {
+        this.loadList();
+    },
+    methods: {
+        formatSeconds(seconds) {
+            const h = Math.floor(seconds / 3600).toString().padStart(2, '0');
+            const m = Math.floor((seconds % 3600) / 60).toString().padStart(2, '0');
+            const s = (seconds % 60).toString().padStart(2, '0');
+            return `${h}:${m}:${s}`;
+        },
+        loadList() {
+            const params = { ...this.query };
+            this.$http.post("/index/answerRanksStat", params).then(res => {
+                if (res.data.code === 0) {
+                    this.list = res.data.data.list;
+                    this.total = res.data.data.total;
+                } else {
+                    this.$message.error(res.data.msg || "查询失败");
+                }
+            });
+        },
+        onSearch() {
+            this.query.page = 1; // 查询时回到第一页
+            this.loadList();
+        },
+        resetQuery() {
+            this.query = {
+                dateType: "day",
+                start_time: "",
+                end_time: "",
+                status: "",
+                page: 1,
+                limit: 10,
+            };
+            this.loadList();
+        },
+        onPageChange(newPage) {
+            this.query.page = newPage;
+            this.loadList();
+        },
+        onSizeChange(newSize) {
+            this.query.limit = newSize;
+            this.query.page = 1; // 每页数量变化时回到第一页
+            this.loadList();
+        }
+    }
+};
+</script>
+
+<style scoped>
+.mt-10 {
+    margin-top: 10px;
+}
+</style>

+ 16 - 21
addons/admin/src/views/dashboard/components/MemberList.vue

@@ -5,8 +5,8 @@
             <el-form-item label="时间类型">
                 <el-select v-model="query.dateType" placeholder="请选择">
                     <el-option label="日" value="day" />
+                    <el-option label="周" value="week" />
                     <el-option label="月" value="month" />
-                    <el-option label="年" value="year" />
                 </el-select>
             </el-form-item>
 
@@ -20,21 +20,6 @@
                     value-format="yyyy-MM-dd" />
             </el-form-item>
 
-            <el-form-item label="类型">
-                <el-select v-model="query.type" placeholder="选择类型" clearable>
-                    <el-option label="开通VIP" :value="1" />
-                    <el-option label="视频课付费" :value="2" />
-                </el-select>
-            </el-form-item>
-
-            <el-form-item label="状态">
-                <el-select v-model="query.status" placeholder="选择状态" clearable>
-                    <el-option label="已完成" :value="1" />
-                    <el-option label="待处理" :value="2" />
-                    <el-option label="失败/取消" :value="3" />
-                </el-select>
-            </el-form-item>
-
             <el-form-item>
                 <el-button type="primary" @click="onSearch">查询</el-button>
                 <el-button @click="resetQuery">重置</el-button>
@@ -44,10 +29,7 @@
         <!-- 表格 -->
         <el-table :data="list" border style="width: 100%; margin-top: 10px;">
             <el-table-column prop="stat_date" :label="dateLabel" />
-            <el-table-column prop="total_count" label="数量" />
-            <el-table-column prop="total_money" label="金额">
-                <template #default="{ row }">{{ parseFloat(row.total_money).toFixed(2) }}</template>
-            </el-table-column>
+            <el-table-column prop="reg_count" label="数量" />
         </el-table>
 
         <!-- 分页 -->
@@ -75,11 +57,22 @@ export default {
             total: 0
         };
     },
+    watch: {
+        query: {
+            handler() {
+                this.query.page = 1; // 筛选条件变化回到第一页
+                this.loadList();
+            },
+            deep: true, // 深度监听对象内部字段
+        },
+    },
     computed: {
         dateLabel() {
             switch (this.query.dateType) {
                 case "day":
                     return "日期";
+                case "week":
+                    return "周数";
                 case "month":
                     return "月份";
                 case "year":
@@ -92,6 +85,8 @@ export default {
             switch (this.query.dateType) {
                 case "year":
                     return "year";
+                case "week":
+                    return "week";
                 case "month":
                     return "month";
                 default:
@@ -105,7 +100,7 @@ export default {
     methods: {
         loadList() {
             const params = { ...this.query };
-            this.$http.post("/account/stats", params).then(res => {
+            this.$http.post("/member/stat", params).then(res => {
                 if (res.data.code === 0) {
                     this.list = res.data.data.list;
                     this.total = res.data.data.total;

+ 14 - 8
addons/admin/src/views/dashboard/components/OrderList.vue

@@ -5,8 +5,8 @@
             <el-form-item label="时间类型">
                 <el-select v-model="query.dateType" placeholder="请选择">
                     <el-option label="日" value="day" />
+                    <el-option label="周" value="week" />
                     <el-option label="月" value="month" />
-                    <el-option label="年" value="year" />
                 </el-select>
             </el-form-item>
 
@@ -20,13 +20,6 @@
                     value-format="yyyy-MM-dd" />
             </el-form-item>
 
-            <el-form-item label="状态">
-                <el-select v-model="query.status" placeholder="选择状态" clearable>
-                    <el-option label="已完成" :value="1" />
-                    <el-option label="待处理" :value="2" />
-                    <el-option label="失败/取消" :value="3" />
-                </el-select>
-            </el-form-item>
 
             <el-form-item>
                 <el-button type="primary" @click="onSearch">查询</el-button>
@@ -85,11 +78,22 @@ export default {
             total: 0
         };
     },
+    watch: {
+        query: {
+            handler() {
+                this.query.page = 1; // 筛选条件变化回到第一页
+                this.loadList();
+            },
+            deep: true, // 深度监听对象内部字段
+        },
+    },
     computed: {
         dateLabel() {
             switch (this.query.dateType) {
                 case "day":
                     return "日期";
+                case "week":
+                    return "周数";
                 case "month":
                     return "月份";
                 case "year":
@@ -102,6 +106,8 @@ export default {
             switch (this.query.dateType) {
                 case "year":
                     return "year";
+                case "week":
+                    return "week";
                 case "month":
                     return "month";
                 default:

+ 6 - 2
addons/admin/src/views/dashboard/workplace.vue

@@ -59,6 +59,9 @@
     <!-- Tab 切换 -->
     <el-card shadow="never" style="margin-top: 20px;">
       <el-tabs v-model="activeTab">
+        <el-tab-pane label="用户访问统计" name="answer_ranks">
+          <AnswerRanks v-if="activeTab === 'answer_ranks'" />
+        </el-tab-pane>
         <el-tab-pane label="视频统计" name="order">
           <OrderList v-if="activeTab === 'order'" defaultType="2" />
         </el-tab-pane>
@@ -76,17 +79,18 @@
 <script>
 import OrderList from "./components/OrderList.vue";
 import MemberList from "./components/MemberList.vue";
+import AnswerRanks from "./components/AnswerRanks.vue";
 import { mapGetters } from "vuex";
 
 export default {
   name: "Workplace",
-  components: { OrderList, MemberList },
+  components: { OrderList, MemberList, AnswerRanks },
   computed: {
     ...mapGetters(["permission"]),
   },
   data() {
     return {
-      activeTab: "order", // 默认营业额
+      activeTab: "answer_ranks", // 默认营业额
       datas: {
         // 用户数据
         users: {

+ 34 - 21
addons/admin/src/views/exam/component/TopicManager.vue

@@ -11,7 +11,7 @@
                     </el-button>
                 </div>
 
-                <el-table :data="topics" row-key="id" height="800" v-loading="loading">
+                <el-table :data="topics" row-key="id" height="600" v-loading="loading">
                     <el-table-column label="排序" width="80">
                         <template slot-scope="{}">
                             <i class="el-icon-sort"></i>
@@ -38,12 +38,24 @@
                     <el-table-column label="分数" prop="score" width="80" />
                     <el-table-column label="答案" width="120">
                         <template slot-scope="{ row }">
-                            <text-ellipsis :text="row.correct_answer" :max-length="10" />
+                            <template slot-scope="{ row }">
+                                <div v-if="row.show_type === 1" class="topic-content">
+                                    <text-ellipsis :text="row.correct_answer" :max-length="10" />
+                                </div>
+                                <div v-else class="topic-content">
+                                    <image-preview size="xs" :images="row.correct_answer" />
+                                </div>
+                            </template>
                         </template>
                     </el-table-column>
                     <el-table-column label="解析" width="120">
                         <template slot-scope="{ row }">
-                            <text-ellipsis :text="row.topic_analysis" :max-length="10" />
+                            <div v-if="row.show_type === 1" class="topic-content">
+                                <text-ellipsis :text="row.topic_analysis" :max-length="10" />
+                            </div>
+                            <div v-else class="topic-content">
+                                <image-preview size="xs" :images="row.topic_analysis" />
+                            </div>
                         </template>
                     </el-table-column>
                     <el-table-column label="操作" width="180" fixed="right">
@@ -95,6 +107,13 @@
                         <el-input-number v-model="form.score" :min="1" :max="100"></el-input-number>
                     </el-form-item>
 
+                    <el-form-item label="允许提交图片答案" prop="answer_type" v-if="form.show_type === 2">
+                        <el-radio-group v-model="form.answer_type">
+                            <el-radio :label="1">是</el-radio>
+                            <el-radio :label="2">否</el-radio>
+                        </el-radio-group>
+                    </el-form-item>
+
                     <!-- 根据题型显示不同的答案输入区域 -->
                     <div v-if="['单选题', '多选题'].includes(form.topic_type)">
                         <el-form-item v-for="(option, index) in options" :key="index"
@@ -134,10 +153,14 @@
                                 placeholder="请输入参考答案"></el-input>
                         </el-form-item>
                     </div>
-
                     <el-form-item label="答案解析" prop="topic_analysis" class="mt-16">
-                        <el-input type="textarea" :rows="3" v-model="form.topic_analysis"
-                            placeholder="请输入题目解析"></el-input>
+                        <div v-if="form.show_type === 1">
+                            <el-input type="textarea" :rows="3" v-model="form.topic_analysis"
+                                placeholder="请输入题目解析"></el-input>
+                        </div>
+                        <div v-else>
+                            <uploadImage :limit="1" v-model="form.topic_analysis"></uploadImage>
+                        </div>
                     </el-form-item>
 
                     <el-form-item>
@@ -328,7 +351,7 @@ export default {
                     type: 'warning'
                 });
 
-                const res = await this.$http.post('/exam/topics/delete', {
+                const res = await this.$http.post('/topics/delete', {
                     id: topic.id
                 });
 
@@ -354,12 +377,12 @@ export default {
             }
             this.form.topic_type = value
         },
-        submitForm() {
+        async submitForm() {
             this.$refs.formRef.validate(async (valid) => {
                 if (!valid) return;
 
                 try {
-                    // 设置选项值
+                    // 保存选项
                     ['A', 'B', 'C', 'D', 'E', 'F'].forEach((letter, index) => {
                         this.form[`answer_${letter}`] = this.options[index] || '';
                     });
@@ -369,6 +392,7 @@ export default {
                     if (this.form.topic_type === '多选题' && Array.isArray(correctAnswer)) {
                         correctAnswer = correctAnswer.join(',');
                     }
+
                     const formData = {
                         ...this.form,
                         correct_answer: correctAnswer,
@@ -378,20 +402,9 @@ export default {
                     const res = await this.$http.post("/topics/edit", formData);
 
                     if (res.data.code === 0) {
-                        // this.$message.success(this.isEdit ? '更新成功' : '添加成功');
-
-                        // 重新加载试题列表
+                        this.$message.success(this.isEdit ? '更新成功' : '添加成功');
                         this.loadTopics();
                         this.cancelForm();
-                        // if (this.isEdit) {
-                        //     // 更新本地数据
-                        //     // this.topics.splice(this.editingIndex, 1, res.data.data);
-                        // } else {
-                        //     // 添加新试题到列表
-                        //     this.topics.unshift(res.data.data);
-                        // }
-
-                        // this.cancelForm();
                         this.$emit('saved');
                     } else {
                         this.$message.error(res.data.msg);

+ 198 - 143
app/Helpers/common.php

@@ -90,7 +90,7 @@ if (!function_exists('array2xml')) {
      */
     function array2xml($arr, $ignore = true, $level = 1)
     {
-        $s     = $level == 1 ? "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\r\n<root>\r\n" : '';
+        $s = $level == 1 ? "<?xml version=\"1.0\" encoding=\"ISO-8859-1\"?>\r\n<root>\r\n" : '';
         $space = str_repeat("\t", $level);
         foreach ($arr as $k => $v) {
             if (!is_array($v)) {
@@ -118,9 +118,10 @@ if (!function_exists('array_merge_multiple')) {
     function array_merge_multiple($array1, $array2)
     {
         $merge = $array1 + $array2;
-        $data  = [];
+        $data = [];
         foreach ($merge as $key => $val) {
-            if (isset($array1[$key])
+            if (
+                isset($array1[$key])
                 && is_array($array1[$key])
                 && isset($array2[$key])
                 && is_array($array2[$key])
@@ -315,7 +316,7 @@ if (!function_exists('datetime')) {
      */
     function datetime($time, $format = 'Y-m-d H:i:s')
     {
-        if($time == '0000-00-00 00:00:00'){
+        if ($time == '0000-00-00 00:00:00') {
             return '';
         }
         $time = is_numeric($time) ? $time : strtotime($time);
@@ -332,10 +333,10 @@ if (!function_exists('dateForWeek')) {
      */
     function dateForWeek($time)
     {
-        if(empty($time)){
+        if (empty($time)) {
             return false;
         }
-        $time = !is_int($time)? strtotime($time) : $time;
+        $time = !is_int($time) ? strtotime($time) : $time;
         $weeks = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
         $week = date("w", $time);
         $cweek = date("w");
@@ -353,12 +354,12 @@ if (!function_exists('dateFormat')) {
      * @param $time 时间戳
      * @return false|string
      */
-    function dateFormat($time, $format='m月d日')
+    function dateFormat($time, $format = 'm月d日')
     {
-        if(empty($time)){
+        if (empty($time)) {
             return false;
         }
-        $time = !is_int($time)? strtotime($time) : $time;
+        $time = !is_int($time) ? strtotime($time) : $time;
         $weeks = ["星期日", "星期一", "星期二", "星期三", "星期四", "星期五", "星期六"];
         $week = date("w", $time);
         $cweek = date("w");
@@ -385,15 +386,15 @@ if (!function_exists('formatHour')) {
      */
     function formatHour($time)
     {
-        if(empty($time)){
+        if (empty($time)) {
             return '00:00';
         }
 
-        $hour = intval($time/3600);
-        $hour = $hour<10?'0'.$hour : $hour;
-        $minute = intval($time%3600/60);
-        $minute = $minute<10?'0'.$minute : $minute;
-        return $hour.':'.$minute;
+        $hour = intval($time / 3600);
+        $hour = $hour < 10 ? '0' . $hour : $hour;
+        $minute = intval($time % 3600 / 60);
+        $minute = $minute < 10 ? '0' . $minute : $minute;
+        return $hour . ':' . $minute;
     }
 }
 
@@ -410,7 +411,7 @@ if (!function_exists('data_auth_sign')) {
     {
         // 数据类型检测
         if (!is_array($data)) {
-            $data = (array)$data;
+            $data = (array) $data;
         }
         // 排序
         ksort($data);
@@ -555,7 +556,7 @@ if (!function_exists('format_time')) {
             '1' => '秒',
         );
         foreach ($format as $key => $val) {
-            $match = floor($interval / (int)$key);
+            $match = floor($interval / (int) $key);
             if (0 != $match) {
                 return $match . $val . '前';
             }
@@ -615,7 +616,7 @@ if (!function_exists('format_cent')) {
      */
     function format_cent($money)
     {
-        return (string)($money * 100);
+        return (string) ($money * 100);
     }
 
 }
@@ -630,11 +631,11 @@ if (!function_exists('format_bank_card')) {
      * @author laravel开发员
      * @date 2019/5/23
      */
-    function format_bank_card($card_no, $is_format = true, $hidden=true)
+    function format_bank_card($card_no, $is_format = true, $hidden = true)
     {
-        if($hidden){
-            $format_card_no = '****'.substr($card_no, -4, 4);
-        }else if ($is_format) {
+        if ($hidden) {
+            $format_card_no = '****' . substr($card_no, -4, 4);
+        } else if ($is_format) {
             // 截取银行卡号前4位
             $prefix = substr($card_no, 0, 4);
             // 截取银行卡号后4位
@@ -643,7 +644,7 @@ if (!function_exists('format_bank_card')) {
             $format_card_no = $prefix . " **** **** **** " . $suffix;
         } else {
             // 4的意思就是每4个为一组
-            $arr            = str_split($card_no, 4);
+            $arr = str_split($card_no, 4);
             $format_card_no = implode(' ', $arr);
         }
         return $format_card_no;
@@ -789,7 +790,7 @@ if (!function_exists('get_zodiac_sign')) {
             array("22" => "射手座"),
             array("22" => "摩羯座")
         );
-        list($sign_start, $sign_name) = each($signs[(int)$month - 1]);
+        list($sign_start, $sign_name) = each($signs[(int) $month - 1]);
         if ($day < $sign_start) {
             list($sign_start, $sign_name) = each($signs[($month - 2 < 0) ? $month = 11 : $month -= 2]);
         }
@@ -813,9 +814,9 @@ if (!function_exists('get_image_url')) {
             return '';
         }
 
-        if($domain){
-            $host = $domain.'/uploads';
-        }else{
+        if ($domain) {
+            $host = $domain . '/uploads';
+        } else {
             $host = request()->header('HOST');
             $https = request()->secure();
             $host = ($https ? 'https://' : 'http://') . $host . '/uploads';
@@ -862,9 +863,9 @@ if (!function_exists('get_format_images')) {
 
         $datas = [];
         foreach ($images as $v) {
-            $url = $key? (isset($v[$key])? $v[$key] : ''):$v;
+            $url = $key ? (isset($v[$key]) ? $v[$key] : '') : $v;
             if ($url) {
-                $datas[] = $key?[$key=>get_image_path($url)]:['url'=>get_image_path($url)];
+                $datas[] = $key ? [$key => get_image_path($url)] : ['url' => get_image_path($url)];
             }
         }
 
@@ -889,13 +890,13 @@ if (!function_exists('get_images_preview')) {
             return [];
         }
 
-        $urls =  is_array($urls)? $urls : json_decode($urls, true);
+        $urls = is_array($urls) ? $urls : json_decode($urls, true);
         foreach ($urls as &$item) {
-            if ($keyName && $level==2) {
+            if ($keyName && $level == 2) {
                 $item[$keyName] = get_image_url($item[$keyName]);
-            } else if($keyName && $level==1){
+            } else if ($keyName && $level == 1) {
                 $item = get_image_url($item[$keyName]);
-            } else if($level==1){
+            } else if ($level == 1) {
                 $item = get_image_url($item);
             }
         }
@@ -936,19 +937,19 @@ if (!function_exists('set_format_content')) {
         }
 
         $domain = request()->header('HOST');
-        if(preg_match("/127/", $domain)){
-            $host = env('IMG_URL','');
-            if(empty($host)){
+        if (preg_match("/127/", $domain)) {
+            $host = env('IMG_URL', '');
+            if (empty($host)) {
                 $https = request()->secure();
                 $host = ($https ? 'https://' : 'http://') . $host . '/uploads';
             }
-        }else{
+        } else {
             $https = request()->secure();
             $host = ($https ? 'https://' : 'http://') . $domain . '/uploads';
         }
 
-        $content = str_replace("{$host}",'/uploads', htmlspecialchars_decode($content));
-        return  $content;
+        $content = str_replace("{$host}", '/uploads', htmlspecialchars_decode($content));
+        return $content;
     }
 }
 
@@ -966,19 +967,19 @@ if (!function_exists('get_format_content')) {
         }
 
         $domain = request()->header('HOST');
-        if(preg_match("/127/", $domain)){
-            $host = env('IMG_URL','');
-            if(empty($host)){
+        if (preg_match("/127/", $domain)) {
+            $host = env('IMG_URL', '');
+            if (empty($host)) {
                 $https = request()->secure();
                 $host = ($https ? 'https://' : 'http://') . $host . '/uploads';
             }
-        }else{
+        } else {
             $https = request()->secure();
             $host = ($https ? 'https://' : 'http://') . $domain . '/uploads';
         }
 
-        $content = str_replace(["\"/uploads","'/uploads"],["\"{$host}","'{$host}"], htmlspecialchars_decode($content));
-        return  $content;
+        $content = str_replace(["\"/uploads", "'/uploads"], ["\"{$host}", "'{$host}"], htmlspecialchars_decode($content));
+        return $content;
     }
 }
 
@@ -995,8 +996,8 @@ if (!function_exists('format_content')) {
             return false;
         }
 
-        $content = str_replace(["\n"],["<br/>"], htmlspecialchars_decode($content));
-        return  get_format_content($content);
+        $content = str_replace(["\n"], ["<br/>"], htmlspecialchars_decode($content));
+        return get_format_content($content);
     }
 }
 
@@ -1010,8 +1011,8 @@ if (!function_exists('get_hash')) {
      */
     function get_hash()
     {
-        $chars   = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-';
-        $random  = $chars[mt_rand(0, 73)] . $chars[mt_rand(0, 73)] . $chars[mt_rand(0, 73)]
+        $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()+-';
+        $random = $chars[mt_rand(0, 73)] . $chars[mt_rand(0, 73)] . $chars[mt_rand(0, 73)]
             . $chars[mt_rand(0, 73)] . $chars[mt_rand(0, 73)];
         $content = uniqid() . $random;
         return sha1($content);
@@ -1076,7 +1077,7 @@ if (!function_exists('get_client_ip')) {
         }
         // IP地址合法验证
         $long = sprintf("%u", ip2long($ip));
-        $ip   = $long ? array($ip, $long) : array('0.0.0.0', 0);
+        $ip = $long ? array($ip, $long) : array('0.0.0.0', 0);
         return $ip[$type];
     }
 
@@ -1100,13 +1101,13 @@ if (!function_exists('get_guid_v4')) {
         }
         // OSX/Linux
         if (function_exists('openssl_random_pseudo_bytes') === true) {
-            $data    = openssl_random_pseudo_bytes(16);
+            $data = openssl_random_pseudo_bytes(16);
             $data[6] = chr(ord($data[6]) & 0x0f | 0x40);    // set version to 0100
             $data[8] = chr(ord($data[8]) & 0x3f | 0x80);    // set bits 6-7 to 10
             return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4));
         }
         // Fallback (PHP 4.2+)
-        mt_srand((double)microtime() * 10000);
+        mt_srand((double) microtime() * 10000);
         $charid = strtolower(md5(uniqid(rand(), true)));
         $hyphen = chr(45);                  // "-"
         $lbrace = $trim ? "" : chr(123);    // "{"
@@ -1181,8 +1182,8 @@ if (!function_exists('is_idcard')) {
      */
     function is_idcard($idno)
     {
-        $idno      = strtoupper($idno);
-        $regx      = '/(^\d{15}$)|(^\d{17}([0-9]|X)$)/';
+        $idno = strtoupper($idno);
+        $regx = '/(^\d{15}$)|(^\d{17}([0-9]|X)$)/';
         $arr_split = array();
         if (!preg_match($regx, $idno)) {
             return false;
@@ -1209,14 +1210,14 @@ if (!function_exists('is_idcard')) {
                 // 检验18位身份证的校验码是否正确。
                 // 校验位按照ISO 7064:1983.MOD 11-2的规定生成,X可以认为是数字10。
                 $arr_int = array(7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2);
-                $arr_ch  = array('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2');
-                $sign    = 0;
+                $arr_ch = array('1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2');
+                $sign = 0;
                 for ($i = 0; $i < 17; $i++) {
-                    $b    = (int)$idno[$i];
-                    $w    = $arr_int[$i];
+                    $b = (int) $idno[$i];
+                    $w = $arr_int[$i];
                     $sign += $b * $w;
                 }
-                $n       = $sign % 11;
+                $n = $sign % 11;
                 $val_num = $arr_ch[$n];
                 if ($val_num != substr($idno, 17, 1)) {
                     return false;
@@ -1376,7 +1377,7 @@ if (!function_exists('message')) {
      */
     function message($msg = "操作成功", $success = true, $data = [], $code = 0, $type = 'json')
     {
-        $result = ['success' => $success, 'msg' => lang($msg), 'data' => $data,'stime'=>time()];
+        $result = ['success' => $success, 'msg' => lang($msg), 'data' => $data, 'stime' => time()];
         if ($success) {
             $result['code'] = 0;
         } else {
@@ -1400,13 +1401,13 @@ if (!function_exists('showJson')) {
      */
     function showJson($msg = "操作成功", $success = true, $data = [], $code = 0, $type = 'json')
     {
-        $result = ['success' => $success, 'msg' => lang($msg), 'data' => $data,'stime'=>time()];
+        $result = ['success' => $success, 'msg' => lang($msg), 'data' => $data, 'stime' => time()];
         if ($success) {
             $result['code'] = 0;
         } else {
             $result['code'] = $code ? $code : -1;
         }
-        return $type=='json'?response()->json($result, 200, [])->setEncodingOptions(256):$result;
+        return $type == 'json' ? response()->json($result, 200, [])->setEncodingOptions(256) : $result;
     }
 }
 
@@ -1421,8 +1422,8 @@ if (!function_exists('num2rmb')) {
      */
     function num2rmb($num)
     {
-        $c1  = "零壹贰叁肆伍陆柒捌玖";
-        $c2  = "分角元拾佰仟万拾佰仟亿";
+        $c1 = "零壹贰叁肆伍陆柒捌玖";
+        $c2 = "分角元拾佰仟万拾佰仟亿";
         $num = round($num, 2);
         $num = $num * 100;
         if (strlen($num) > 10) {
@@ -1443,23 +1444,23 @@ if (!function_exists('num2rmb')) {
             } else {
                 $c = $p1 . $c;
             }
-            $i   = $i + 1;
+            $i = $i + 1;
             $num = $num / 10;
-            $num = (int)$num;
+            $num = (int) $num;
             if ($num == 0) {
                 break;
             }
         }
-        $j    = 0;
+        $j = 0;
         $slen = strlen($c);
         while ($j < $slen) {
             $m = substr($c, $j, 6);
             if ($m == '零元' || $m == '零万' || $m == '零亿' || $m == '零零') {
-                $left  = substr($c, 0, $j);
+                $left = substr($c, 0, $j);
                 $right = substr($c, $j + 3);
-                $c     = $left . $right;
-                $j     = $j - 3;
-                $slen  = $slen - 3;
+                $c = $left . $right;
+                $j = $j - 3;
+                $slen = $slen - 3;
             }
             $j = $j + 3;
         }
@@ -1531,8 +1532,8 @@ if (!function_exists('strip_html_tags')) {
         // 将空格替换成空
         $str = str_replace("&nbsp;", "", $str);
         // 函数剥去字符串中的 HTML、XML 以及 PHP 的标签,获取纯文本内容
-        $str  = strip_tags($str);
-        $str  = str_replace(array("\n", "\r\n", "\r"), ' ', $str);
+        $str = strip_tags($str);
+        $str = str_replace(array("\n", "\r\n", "\r"), ' ', $str);
         $preg = '/<script[\s\S]*?<\/script>/i';
         // 剥离JS代码
         $str = preg_replace($preg, "", $str, -1);
@@ -1565,10 +1566,10 @@ if (!function_exists('sub_str')) {
         } elseif (function_exists('iconv_substr')) {
             $slice = iconv_substr($str, $start, $length, $charset);
         } else {
-            $re['utf-8']  = "/[\x01-\x7f]|[\xc2-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xff][\x80-\xbf]{3}/";
+            $re['utf-8'] = "/[\x01-\x7f]|[\xc2-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xff][\x80-\xbf]{3}/";
             $re['gb2312'] = "/[\x01-\x7f]|[\xb0-\xf7][\xa0-\xfe]/";
-            $re['gbk']    = "/[\x01-\x7f]|[\x81-\xfe][\x40-\xfe]/";
-            $re['big5']   = "/[\x01-\x7f]|[\x81-\xfe]([\x40-\x7e]|\xa1-\xfe])/";
+            $re['gbk'] = "/[\x01-\x7f]|[\x81-\xfe][\x40-\xfe]/";
+            $re['big5'] = "/[\x01-\x7f]|[\x81-\xfe]([\x40-\x7e]|\xa1-\xfe])/";
             preg_match_all($re[$charset], $str, $match);
             $slice = join("", array_slice($match[0], $start, $length));
         }
@@ -1594,7 +1595,7 @@ if (!function_exists('save_image')) {
             return false;
         }
         $save_dir = trim($save_dir, "/");
-        $imgExt   = pathinfo($img_url, PATHINFO_EXTENSION);
+        $imgExt = pathinfo($img_url, PATHINFO_EXTENSION);
 
 
         // 是否是本站图片
@@ -1785,8 +1786,8 @@ if (!function_exists('upload_image')) {
         }
 
         // 文件名称
-        $file_name = ($userId? $userId.'_'.str_replace('=','',base64_encode($userId.$original_name)): str_replace('=','',base64_encode($original_name)));
-        $file_name =  $file_name. '.' . $ext;
+        $file_name = ($userId ? $userId . '_' . str_replace('=', '', base64_encode($userId . $original_name)) : str_replace('=', '', base64_encode($original_name)));
+        $file_name = $file_name . '.' . $ext;
 
         // 重命名保存
         $path = $file->move($file_dir, $file_name);
@@ -1797,12 +1798,12 @@ if (!function_exists('upload_image')) {
         // 返回结果
         $result = [
             'img_original_name' => $original_name,
-            'img_ext'           => $ext,
-            'img_real_path'     => $real_path,
-            'img_type'          => $type,
-            'img_size'          => $size,
-            'img_name'          => $file_name,
-            'img_path'          => $file_path,
+            'img_ext' => $ext,
+            'img_real_path' => $real_path,
+            'img_type' => $type,
+            'img_size' => $size,
+            'img_name' => $file_name,
+            'img_path' => $file_path,
         ];
         return message(MESSAGE_OK, true, $result);
     }
@@ -1874,12 +1875,12 @@ if (!function_exists('upload_file')) {
         // 返回结果
         $result = [
             'file_original_name' => $original_name,
-            'file_ext'           => $ext,
-            'file_real_path'     => $real_path,
-            'file_type'          => $type,
-            'file_size'          => $size,
-            'file_name'          => $file_name,
-            'file_path'          => $file_path,
+            'file_ext' => $ext,
+            'file_real_path' => $real_path,
+            'file_type' => $type,
+            'file_size' => $size,
+            'file_name' => $file_name,
+            'file_path' => $file_path,
         ];
         return message(MESSAGE_OK, true, $result);
     }
@@ -1950,12 +1951,12 @@ if (!function_exists('upload_video')) {
         // 返回结果
         $result = [
             'file_original_name' => $original_name,
-            'file_ext'           => $ext,
-            'file_real_path'     => $real_path,
-            'file_type'          => $type,
-            'file_size'          => $size,
-            'file_name'          => $file_name,
-            'file_path'          => $file_path,
+            'file_ext' => $ext,
+            'file_real_path' => $real_path,
+            'file_type' => $type,
+            'file_size' => $size,
+            'file_name' => $file_name,
+            'file_path' => $file_path,
         ];
         return message(MESSAGE_OK, true, $result);
     }
@@ -2011,7 +2012,7 @@ if (!function_exists('get_tree')) {
                 $tempArr[] = $data;
             } else if ($currentPid == 0 && $pid == 0) {
                 $data['children'] = get_tree($datas, $v['id'], []);
-                $tempArr[$id]     = $data;
+                $tempArr[$id] = $data;
             }
         }
 
@@ -2030,7 +2031,7 @@ if (!function_exists('getSign')) {
         $str .= '&key=' . ($key ? $key : env('APP_SIGN_KEY', 'app&688'));
         $str = md5($str);
         // MD5 运算
-        $sign = md5($str.substr($str,0,6));
+        $sign = md5($str . substr($str, 0, 6));
         $sign .= substr($str, 2, 4);
         $sign = strtoupper($sign);
 
@@ -2048,10 +2049,10 @@ if (!function_exists('arrayToStr')) {
             $array = array();
 
             foreach ($params as $key => &$value) {
-                if($key != 'sign'){
+                if ($key != 'sign') {
                     // 过滤无法匹配校验的参数类型
-                    if (!is_array($value) && $value !== 'undefined' && strtolower($value) != 'null' && !empty($value)  && !preg_match("/^[0-9]{1,9}\.[0-9]{8,}$/", $value)) {
-                        $array[] = $key . '=' .trim($value);
+                    if (!is_array($value) && $value !== 'undefined' && strtolower($value) != 'null' && !empty($value) && !preg_match("/^[0-9]{1,9}\.[0-9]{8,}$/", $value)) {
+                        $array[] = $key . '=' . trim($value);
                     }
                 }
             }
@@ -2070,7 +2071,7 @@ if (!function_exists('arrayToUrl')) {
             $array = array();
             foreach ($params as $key => $value) {
                 if ($value != 'undefined') {
-                    $array[] = $key . '=' . (is_array($value) ? json_encode($value,JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE) : $value);
+                    $array[] = $key . '=' . (is_array($value) ? json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : $value);
                 }
             }
             $string = implode("&", $array);
@@ -2090,13 +2091,13 @@ if (!function_exists('httpRequest')) {
      * @return mixed
      * @author wesmiler
      */
-    function httpRequest($url, $data = '', $type = 'post', $cookie = '', $timeout = 60, $header=[])
+    function httpRequest($url, $data = '', $type = 'post', $cookie = '', $timeout = 60, $header = [])
     {
         try {
             set_time_limit($timeout);
             $data = $data && is_array($data) ? http_build_query($data) : $data;
-            $url  = strtolower($type) == 'get' ? $url . (strpos($url, '?') === false ? '?' : '&') . $data : $url;
-            $ch   = curl_init($url);
+            $url = strtolower($type) == 'get' ? $url . (strpos($url, '?') === false ? '?' : '&') . $data : $url;
+            $ch = curl_init($url);
             if (!empty($cookie)) {
                 curl_setopt($ch, CURLOPT_COOKIE, $cookie);
             }
@@ -2193,7 +2194,7 @@ if (!function_exists('moneyFormat')) {
     }
 }
 
-if(!function_exists('getChatKey')){
+if (!function_exists('getChatKey')) {
     /**
      * 获取聊天窗口KEY
      * @param $fromUserId
@@ -2202,7 +2203,7 @@ if(!function_exists('getChatKey')){
      */
     function getChatKey($fromUserId, $toUserId)
     {
-        if(empty($fromUserId) || empty($toUserId)){
+        if (empty($fromUserId) || empty($toUserId)) {
             return false;
         }
 
@@ -2212,7 +2213,7 @@ if(!function_exists('getChatKey')){
     }
 }
 
-if(!function_exists('getDistance')) {
+if (!function_exists('getDistance')) {
     /**
      * 计算地图坐标距离/米
      * @param $lat
@@ -2224,32 +2225,37 @@ if(!function_exists('getDistance')) {
     function getDistance($lat, $lng, $toLat, $toLng)
     {
         return ROUND(6378.138 * 2 * ASIN(
-                SQRT(
-                    POW(
-                        SIN(
-                            ($lat * PI() / 180 - $toLat * PI() / 180
-                            ) / 2
-                        ),
-                        2
-                    ) + COS($lat * PI() / 180) * COS($toLat * PI() / 180) * POW(
-                        SIN(
-                            ($lng * PI() / 180 - $toLng * PI() / 180
-                            ) / 2
-                        ), 2))) * 1000);
+            SQRT(
+                POW(
+                    SIN(
+                        ($lat * PI() / 180 - $toLat * PI() / 180
+                        ) / 2
+                    ),
+                    2
+                ) + COS($lat * PI() / 180) * COS($toLat * PI() / 180) * POW(
+                    SIN(
+                        ($lng * PI() / 180 - $toLng * PI() / 180
+                        ) / 2
+                    ),
+                    2
+                )
+            )
+        ) * 1000);
 
     }
 }
 
-if(!function_exists('getParents')){
+if (!function_exists('getParents')) {
     /**
      * 获取分销上级用户ID
      * @param $parents
      * @return array|false
      */
-    function getParents($parents, $level=3){
-        $parents = $parents? explode(',', $parents) : [];
+    function getParents($parents, $level = 3)
+    {
+        $parents = $parents ? explode(',', $parents) : [];
         $parents = array_filter($parents);
-        if(empty($parents)){
+        if (empty($parents)) {
             return false;
         }
 
@@ -2258,35 +2264,37 @@ if(!function_exists('getParents')){
     }
 }
 
-if(!function_exists('getMillSecondTime')){
+if (!function_exists('getMillSecondTime')) {
     /**
      * 获取毫秒时间戳
      * @return float
      */
-    function getMillSecondTime() {
+    function getMillSecondTime()
+    {
         list($msec, $sec) = explode(' ', microtime());
-        return (float)sprintf('%.0f', (floatval($msec) + floatval($sec)) * 1000);
+        return (float) sprintf('%.0f', (floatval($msec) + floatval($sec)) * 1000);
     }
 }
 
-if(!function_exists('getPriceData')){
+if (!function_exists('getPriceData')) {
     /**
      * 格式化价格数据
      * @param $prices
      * @return array|false|string[]
      */
-    function getPriceData($prices) {
+    function getPriceData($prices)
+    {
         $datas = [];
-        $prices = $prices? explode('|', $prices) : [];
-        if($prices){
-            foreach ($prices as $item){
-                $data = $item? explode(':',$item) : [];
-                $num = isset($data[0]) && $data[0]? $data[0] : '';
-                $price = isset($data[1])? $data[1] : 0;
-                if($num && $price){
+        $prices = $prices ? explode('|', $prices) : [];
+        if ($prices) {
+            foreach ($prices as $item) {
+                $data = $item ? explode(':', $item) : [];
+                $num = isset($data[0]) && $data[0] ? $data[0] : '';
+                $price = isset($data[1]) ? $data[1] : 0;
+                if ($num && $price) {
                     $datas[] = [
-                        'distance'=> $num,
-                        'price'=> $price,
+                        'distance' => $num,
+                        'price' => $price,
                     ];
                 }
             }
@@ -2310,20 +2318,67 @@ if (!function_exists('api_decrypt')) {
 }
 
 
-if (!function_exists('format_message')){
+if (!function_exists('format_message')) {
     /**
      * 格式化消息内容
      * @param $message
      * @return false|string|string[]|null
      */
-    function format_message($message){
-        if(empty($message)){
+    function format_message($message)
+    {
+        if (empty($message)) {
             return false;
         }
         $pattern = "/((https|http|ftp):\/\/((\w+(\-)?\w+(\.)){0,2}\w+\.[a-z]{2,3}(\/[a-zA-Z0-9\_\.]{1,50}(\.php|\.html|\.js|\.htm|\.apk)?){0,5})([\?&]\w+=\w*){0,5}(\s?))/u";
-        $message = preg_replace($pattern,'<a href="$1" target="_blank">$1</a>', $message);
+        $message = preg_replace($pattern, '<a href="$1" target="_blank">$1</a>', $message);
         return format_content($message);
     }
 }
 
 
+
+if (!function_exists('format_image_field')) {
+
+    /**
+     * 格式化图片字段(支持单图、多图)
+     * @param string|array $images 数据库存储的图片(可能是字符串或数组)
+     * @param string $domain 域名,可选
+     * @return string|array
+     */
+    function format_image_field($images, $domain = '')
+    {
+        if (empty($images)) {
+            return '';
+        }
+
+        // 如果是 JSON 格式存储(多图)
+        if (is_string($images) && (strpos($images, '[') === 0 || strpos($images, '{') === 0)) {
+            $arr = json_decode($images, true);
+            if (is_array($arr)) {
+                $arr = array_map(function ($img) use ($domain) {
+                    return get_image_url($img, $domain);
+                }, $arr);
+                return json_encode($arr, JSON_UNESCAPED_UNICODE);
+            }
+        }
+
+        // 如果是用逗号分隔的字符串(多图)
+        if (is_string($images) && strpos($images, ',') !== false) {
+            $arr = explode(',', $images);
+            $arr = array_map(function ($img) use ($domain) {
+                return get_image_url($img, $domain);
+            }, $arr);
+            return implode(',', $arr);
+        }
+
+        // 如果是数组
+        if (is_array($images)) {
+            return array_map(function ($img) use ($domain) {
+                return get_image_url($img, $domain);
+            }, $images);
+        }
+
+        // 单图字符串
+        return get_image_url($images, $domain);
+    }
+}

+ 15 - 0
app/Http/Controllers/Admin/IndexController.php

@@ -12,6 +12,7 @@
 namespace App\Http\Controllers\Admin;
 
 use App\Services\Common\AccountService;
+use App\Services\Common\AnswerRanksService;
 use App\Services\Common\BalanceLogService;
 use App\Services\Common\DepositService;
 use App\Services\Common\GoodsService;
@@ -21,6 +22,7 @@ use App\Services\Common\OrderService;
 use App\Services\Common\UserService;
 use App\Services\RedisService;
 use App\utils\TimeUtils;
+use Request;
 
 /**
  * 系统主页控制器
@@ -149,4 +151,17 @@ class IndexController extends Backend
 
         return message(1010, true, $datas);
     }
+
+
+    public function answerRanksStat()
+    {
+        $params = Request()->only(['dateType', 'start_time', 'end_time', 'page', 'limit']);
+
+        $service = AnswerRanksService::make();
+        $data = $service->getAnswerRanks($params);
+        return response()->json([
+            'code' => 0,
+            'data' => $data
+        ]);
+    }
 }

+ 12 - 4
app/Services/Common/AccountService.php

@@ -275,22 +275,30 @@ class AccountService extends BaseService
         switch ($dateType) {
             case 'year':
                 $format = '%Y';
+                $groupRaw = "FROM_UNIXTIME(create_time, '{$format}')";
                 break;
             case 'month':
                 $format = '%Y-%m';
+                $groupRaw = "FROM_UNIXTIME(create_time, '{$format}')";
+                break;
+            case 'week':
+                // 按周统计,显示格式 YYYY-WW
+                $groupRaw = "CONCAT(YEAR(FROM_UNIXTIME(create_time)), '-', LPAD(WEEK(FROM_UNIXTIME(create_time), 1), 2, '0'))";
                 break;
             case 'day':
             default:
                 $format = '%Y-%m-%d';
+                $groupRaw = "FROM_UNIXTIME(create_time, '{$format}')";
+                break;
         }
 
         // 基础查询
         $query = DB::table('account_logs')
             ->selectRaw("
-                FROM_UNIXTIME(create_time, '{$format}') as stat_date,
-                COUNT(*) as total_count,
-                SUM(money) as total_money
-            ")
+            {$groupRaw} as stat_date,
+            COUNT(*) as total_count,
+            SUM(money) as total_money
+        ")
             ->whereBetween('create_time', [$startTimestamp, $endTimestamp]);
 
         // 条件过滤

+ 108 - 0
app/Services/Common/AnswerRanksService.php

@@ -0,0 +1,108 @@
+<?php
+// +----------------------------------------------------------------------
+// | LARAVEL8.0 框架 [ LARAVEL ][ RXThinkCMF ]
+// +----------------------------------------------------------------------
+// | 版权所有 2017~2021 LARAVEL研发中心
+// +----------------------------------------------------------------------
+// | 官方网站: http://www.laravel.cn
+// +----------------------------------------------------------------------
+// | Author: laravel开发员 <laravel.qq.com>
+// +----------------------------------------------------------------------
+
+namespace App\Services\Common;
+
+use App\Models\UserModel;
+use App\Services\BaseService;
+use App\Services\ConfigService;
+use Illuminate\Support\Facades\DB;
+
+/**
+ * 用户管理-服务类
+ * @author laravel开发员
+ * @since 2020/11/11
+ * Class UserService
+ * @package App\Services\Common
+ */
+class AnswerRanksService extends BaseService
+{
+    /**
+     * 构造函数
+     * @author laravel开发员
+     * @since 2020/11/11
+     * UserService constructor.
+     */
+    public function __construct()
+    {
+    }
+
+    /**
+     * 静态入口
+     */
+    public static function make()
+    {
+        if (!self::$instance) {
+            self::$instance = new static();
+        }
+        return self::$instance;
+    }
+
+
+    public function getAnswerRanks(array $params)
+    {
+        $dateType = $params['dateType'] ?? 'day';
+        $page = max(1, (int) ($params['page'] ?? 1));
+        $limit = max(1, (int) ($params['limit'] ?? 10));
+
+        // 时间戳
+        $startTimestamp = !empty($params['start_time']) ? strtotime($params['start_time']) : strtotime(date('2000-01-01 00:00:00'));
+        $endTimestamp = !empty($params['end_time']) ? strtotime($params['end_time']) : time();
+        if ($endTimestamp < $startTimestamp) {
+            $endTimestamp = $startTimestamp;
+        }
+
+        // 分组
+        switch ($dateType) {
+            case 'month':
+                $groupRaw = "FROM_UNIXTIME(create_time, '%Y-%m')";
+                break;
+            case 'year':
+                $groupRaw = "FROM_UNIXTIME(create_time, '%Y')";
+                break;
+            case 'week':
+                $groupRaw = "CONCAT(YEAR(FROM_UNIXTIME(create_time)),'-',LPAD(WEEK(FROM_UNIXTIME(create_time),1),2,'0'))";
+                break;
+            case 'day':
+            default:
+                $groupRaw = "FROM_UNIXTIME(create_time, '%Y-%m-%d')";
+                break;
+        }
+
+        // 基础查询
+        $query = DB::table('member_answer_ranks')
+            ->selectRaw("
+                {$groupRaw} as stat_date,
+                SUM(answer_count) as total_count,
+                SUM(answer_time) as total_time
+            ")
+            ->whereBetween('create_time', [$startTimestamp, $endTimestamp])
+            ->where('status', 1)
+            ->where('mark', 1)
+            ->groupBy('stat_date')
+            ->orderBy('stat_date', 'desc');
+
+        // 总条数
+        $total = DB::table(DB::raw("({$query->toSql()}) as t"))
+            ->mergeBindings($query)
+            ->count();
+
+        // 分页
+        $list = $query->forPage($page, $limit)->get();
+
+        return [
+            'list' => $list,
+            'total' => $total,
+            'page' => $page,
+            'limit' => $limit,
+        ];
+    }
+}

+ 11 - 3
app/Services/Common/MemberService.php

@@ -421,17 +421,25 @@ class MemberService extends BaseService
         $page = max(1, (int) ($params['page'] ?? 1));
         $limit = max(1, (int) ($params['limit'] ?? 10));
 
-        // 根据 dateType 选择时间格式
+        // 分组日期格式
         switch ($dateType) {
             case 'month':
                 $format = '%Y-%m';
+                $groupRaw = "FROM_UNIXTIME(create_time, '{$format}')";
                 break;
             case 'year':
                 $format = '%Y';
+                $groupRaw = "FROM_UNIXTIME(create_time, '{$format}')";
+                break;
+            case 'week':
+                // 按周统计,格式 YYYY-WW
+                $groupRaw = "CONCAT(YEAR(FROM_UNIXTIME(create_time)), '-', LPAD(WEEK(FROM_UNIXTIME(create_time), 1), 2, '0'))";
                 break;
             case 'day':
             default:
                 $format = '%Y-%m-%d';
+                $groupRaw = "FROM_UNIXTIME(create_time, '{$format}')";
+                break;
         }
 
         // 统一转为时间戳
@@ -439,7 +447,7 @@ class MemberService extends BaseService
             ? (is_numeric($params['start_time'])
                 ? (int) $params['start_time']
                 : strtotime($params['start_time']))
-            : strtotime(date('2000-01-01 00:00:00')); // 默认当月1号
+            : strtotime(date('2000-01-01 00:00:00')); // 默认起始时间
 
         $endTimestamp = !empty($params['end_time'])
             ? (is_numeric($params['end_time'])
@@ -453,7 +461,7 @@ class MemberService extends BaseService
 
         // 基础查询
         $baseQuery = DB::table('member')
-            ->selectRaw("FROM_UNIXTIME(create_time, '{$format}') as stat_date, COUNT(*) as reg_count")
+            ->selectRaw("{$groupRaw} as stat_date, COUNT(*) as reg_count")
             ->whereBetween('create_time', [$startTimestamp, $endTimestamp])
             ->groupBy('stat_date')
             ->orderBy('stat_date', 'desc');

+ 67 - 2
app/Services/Exam/TopicService.php

@@ -45,12 +45,33 @@ class TopicService extends BaseService
 
         $list = $query->orderBy('sort', 'desc')->paginate($pageSize > 0 ? $pageSize : 9999999);
         $list = $list ? $list->toArray() : [];
+        // 要处理的字段
+        $fields = ['poster', 'topic_analysis', 'topic_name', 'correct_answer'];
+
+        $items = isset($list['data']) ? $list['data'] : [];
+
+        foreach ($items as &$item) {
+            if (!empty($item['show_type']) && (int) $item['show_type'] === 2) {
+                // 图片型 → 转为完整图片路径(支持多图)
+                foreach ($fields as $field) {
+                    if (!empty($item[$field])) {
+                        $item[$field] = format_image_field($item[$field]);
+                    }
+                }
+            } else {
+                // 文本型 → 保持原样(如果你想裁剪空格可以加 trim)
+                foreach ($fields as $field) {
+                    if (!empty($item[$field])) {
+                        $item[$field] = trim($item[$field]);
+                    }
+                }
+            }
+        }
         return [
             'pageSize' => $pageSize,
             'total' => isset($list['total']) ? $list['total'] : 0,
-            'list' => isset($list['data']) ? $list['data'] : []
+            'list' => $items
         ];
-        return message("操作成功", true, $list);
     }
 
     /**
@@ -102,4 +123,48 @@ class TopicService extends BaseService
             return message("排序更新失败:" . $e->getMessage(), false);
         }
     }
+    /**
+     * 编辑或新增视频集
+     */
+    public function edit()
+    {
+        $data = request()->all();
+
+        // 定义需要处理的字段
+        $fields = ['poster', 'topic_analysis', 'topic_name', 'correct_answer'];
+
+        foreach ($fields as $field) {
+            if (!empty($data[$field])) {
+                // 如果 show_type == 2 → 处理为图片路径
+                if (!empty($data['show_type']) && (int) $data['show_type'] === 2) {
+                    if (is_array($data[$field])) {
+                        $data[$field] = array_map(function ($img) {
+                            return get_image_path(trim($img));
+                        }, $data[$field]);
+                        $data[$field] = implode(',', $data[$field]); // 多图用逗号拼接
+                    } else {
+                        $data[$field] = get_image_path(trim($data[$field]));
+                    }
+                } else {
+                    // show_type != 2 → 文本,直接 trim
+                    if (is_array($data[$field])) {
+                        $data[$field] = implode(',', array_map('trim', $data[$field]));
+                    } else {
+                        $data[$field] = trim($data[$field]);
+                    }
+                }
+            }
+        }
+
+        $id = $data['id'] ?? 0;
+
+        // 默认字段处理
+        $data['update_time'] = time();
+        if (!$id) {
+            $data['create_time'] = time();
+        }
+
+        return parent::edit($data); // 调用父类的 edit 方法
+    }
+
 }

+ 1 - 0
routes/web.php

@@ -67,6 +67,7 @@ Route::get('/index/getUserInfo', [IndexController::class, 'getUserInfo']);
 Route::post('/index/updateUserInfo', [IndexController::class, 'updateUserInfo']);
 Route::post('/index/updatePwd', [IndexController::class, 'updatePwd']);
 Route::post('/index/statistics', [IndexController::class, 'statistics']);
+Route::post('/index/answerRanksStat', [IndexController::class, 'answerRanksStat']);
 
 // 用户管理
 Route::get('/user/index', [UserController::class, 'index']);