xiongzhu преди 4 години
родител
ревизия
dcdc77ff16

+ 22 - 0
src/main/java/com/izouma/dangjian/domain/VisitStat.java

@@ -0,0 +1,22 @@
+package com.izouma.dangjian.domain;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+import javax.persistence.Entity;
+import java.time.LocalDate;
+
+@Data
+@Entity
+@AllArgsConstructor
+@NoArgsConstructor
+@Builder
+public class VisitStat extends BaseEntity {
+    private LocalDate date;
+
+    private Long userId;
+
+    private long num;
+}

+ 9 - 0
src/main/java/com/izouma/dangjian/repo/OrderRepo.java

@@ -7,10 +7,19 @@ import org.springframework.data.jpa.repository.Modifying;
 import org.springframework.data.jpa.repository.Query;
 
 import javax.transaction.Transactional;
+import java.time.LocalDateTime;
 
 public interface OrderRepo extends JpaRepository<Order, Long>, JpaSpecificationExecutor<Order> {
     @Query("update Order t set t.del = true where t.id = ?1")
     @Modifying
     @Transactional
     void softDelete(Long id);
+
+    @Query("select count(o) from Order o join User u on o.userId = u.id " +
+            "where u.invitor = ?1")
+    long countInvitor(Long userId);
+
+    @Query("select count(o) from Order o join User u on o.userId = u.id " +
+            "where u.invitor = ?1 and o.createdAt >= ?2 and o.createdAt <= ?3")
+    long countInvitor(Long userId, LocalDateTime start, LocalDateTime end);
 }

+ 14 - 0
src/main/java/com/izouma/dangjian/repo/VisitStatRepo.java

@@ -0,0 +1,14 @@
+package com.izouma.dangjian.repo;
+
+import com.izouma.dangjian.domain.VisitStat;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.time.LocalDate;
+import java.util.List;
+import java.util.Optional;
+
+public interface VisitStatRepo extends JpaRepository<VisitStat, Long> {
+    Optional<VisitStat> findByUserIdAndDate(Long userId, LocalDate date);
+
+    List<VisitStat> findByUserIdAndDateBetween(Long userId, LocalDate start, LocalDate end);
+}

+ 1 - 0
src/main/java/com/izouma/dangjian/security/WebSecurityConfig.java

@@ -82,6 +82,7 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
                 .antMatchers("/banner/all").permitAll()
                 .antMatchers("/banner/get/*").permitAll()
                 .antMatchers("/sysConfig/get/*").permitAll()
+                .antMatchers("/visitStat/**").permitAll()
                 // all other requests need to be authenticated
                 .anyRequest().authenticated().and()
                 // make sure we use stateless session; session won't be used to

+ 56 - 0
src/main/java/com/izouma/dangjian/service/VisitStatService.java

@@ -0,0 +1,56 @@
+package com.izouma.dangjian.service;
+
+import com.izouma.dangjian.domain.VisitStat;
+import com.izouma.dangjian.repo.OrderRepo;
+import com.izouma.dangjian.repo.VisitStatRepo;
+import com.izouma.dangjian.utils.DateTimeUtils;
+import lombok.AllArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Service
+@AllArgsConstructor
+public class VisitStatService {
+    private VisitStatRepo visitStatRepo;
+    private OrderRepo     orderRepo;
+
+    public void add(Long userId, LocalDate date) {
+        VisitStat stat = visitStatRepo.findByUserIdAndDate(userId, date).orElse(new VisitStat(date, userId, 0));
+        stat.setNum(stat.getNum() + 1);
+        visitStatRepo.save(stat);
+    }
+
+    public Map<String, Object> stat1(Long userId, LocalDate start, LocalDate end) {
+        long visitNum = visitStatRepo.findByUserIdAndDateBetween(userId, start, end).stream()
+                .mapToLong(VisitStat::getNum)
+                .sum();
+        long orderNum = orderRepo.countInvitor(userId);
+        Map<String, Object> map = new HashMap<>();
+        map.put("visitNum", visitNum);
+        map.put("orderNum", orderNum);
+        return map;
+    }
+
+    public Map<String, Object> stat2(Long userId, LocalDate start, LocalDate end) {
+        List<Long> visitNum = new ArrayList<>();
+        List<Long> orderNum = new ArrayList<>();
+        List<String> labels = new ArrayList<>();
+        for (int i = 0; i <= ChronoUnit.DAYS.between(start, end); i++) {
+            LocalDate date = start.plusDays(i);
+            labels.add(DateTimeUtils.format(date, "MM-dd"));
+            visitNum.add(visitStatRepo.findByUserIdAndDate(userId, date).map(VisitStat::getNum).orElse(0L));
+            orderNum.add(orderRepo.countInvitor(userId, date.atStartOfDay(), date.atTime(23, 59, 59)));
+        }
+        Map<String, Object> map = new HashMap<>();
+        map.put("visitNum", visitNum);
+        map.put("orderNum", orderNum);
+        map.put("labels", labels);
+        return map;
+    }
+}

+ 34 - 0
src/main/java/com/izouma/dangjian/web/VisitStatController.java

@@ -0,0 +1,34 @@
+package com.izouma.dangjian.web;
+
+import com.izouma.dangjian.service.VisitStatService;
+import com.izouma.dangjian.utils.SecurityUtils;
+import lombok.AllArgsConstructor;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.LocalDate;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/visitStat")
+@AllArgsConstructor
+public class VisitStatController {
+    private VisitStatService visitStatService;
+
+    @GetMapping("/add")
+    public void add(Long userId) {
+        visitStatService.add(userId, LocalDate.now());
+    }
+
+    @GetMapping("/stat1")
+    public Map<String, Object> stat1(@RequestParam LocalDate start, @RequestParam LocalDate end) {
+        return visitStatService.stat1(SecurityUtils.getAuthenticatedUser().getId(), start, end);
+    }
+
+    @GetMapping("/stat2")
+    public Map<String, Object> stat2(@RequestParam LocalDate start, @RequestParam LocalDate end) {
+        return visitStatService.stat2(SecurityUtils.getAuthenticatedUser().getId(), start, end);
+    }
+}

+ 2 - 0
src/main/mobile/package.json

@@ -11,6 +11,7 @@
     "@capacitor/core": "^2.4.7",
     "@chenfengyuan/vue-qrcode": "^1.0.2",
     "axios": "^0.21.1",
+    "chart.js": "^2.9.4",
     "core-js": "^3.6.5",
     "date-fns": "^2.19.0",
     "dayjs": "^1.10.4",
@@ -24,6 +25,7 @@
     "vconsole-webpack-plugin": "^1.5.2",
     "vue": "^2.6.11",
     "vue-bus": "^1.2.1",
+    "vue-chartjs": "^3.5.1",
     "vue-cli-plugin-style-resources-loader": "^0.1.5",
     "vue-clipboard2": "^0.3.1",
     "vue-meta": "^2.4.0",

+ 9 - 0
src/main/mobile/src/components/chart.js

@@ -0,0 +1,9 @@
+import { Line } from 'vue-chartjs';
+
+export default {
+    extends: Line,
+    props: ['data', 'options'],
+    mounted() {
+        this.renderChart(this.data, this.options);
+    }
+};

+ 1 - 0
src/main/mobile/src/main.js

@@ -53,4 +53,5 @@ let invitor = new URLSearchParams(window.location.search).get('invitor');
 if (invitor) {
     invitor = invitor.replace(/\/$/, '');
     store.commit('setInvitor', invitor);
+    vm.$http.get('/visitStat/add', { userId: invitor });
 }

+ 186 - 3
src/main/mobile/src/views/stat.vue

@@ -1,11 +1,194 @@
 <template>
-    <div>stat</div>
+    <div>
+        <van-notice-bar
+            text="请及时完善您的信息,便于精准统计!"
+            :color="$theme.prim"
+            v-if="!(userInfo && userInfo.realName)"
+        >
+            <van-button size="mini" slot="right-icon" :color="$theme.prim" @click="$router.push({ name: 'profile' })">
+                去完善
+            </van-button>
+        </van-notice-bar>
+        <div class="stat-title">概况统计</div>
+        <van-tabs type="card" class="stat-tabs" v-model="tab1">
+            <van-tab title="今日"></van-tab>
+            <van-tab title="昨日"></van-tab>
+            <van-tab title="近7日"></van-tab>
+            <van-tab title="近30日"></van-tab>
+        </van-tabs>
+        <div class="stat">
+            <div class="col">
+                <div class="value">{{ stat1.visitNum }}</div>
+                <div class="label">打开次数</div>
+            </div>
+            <div class="col">
+                <div class="value">{{ stat1.orderNum }}</div>
+                <div class="label">成交笔数</div>
+            </div>
+        </div>
+        <div class="stat-title">趋势统计</div>
+        <van-tabs type="card" class="stat-tabs" v-model="tab2">
+            <van-tab title="近7日"></van-tab>
+            <van-tab title="近30日"></van-tab>
+        </van-tabs>
+        <div style="padding: 0 16px; margin-top: 15px">
+            <chart
+                :data="chartData"
+                :options="{ responsive: true, maintainAspectRatio: false }"
+                v-if="chartData"
+            ></chart>
+        </div>
+    </div>
 </template>
 <script>
+import { mapState } from 'vuex';
+import { format, parse, addDays, startOfWeek, startOfMonth, endOfWeek, endOfMonth, startOfDay } from 'date-fns';
+import chart from '../components/chart';
 export default {
+    components: { chart },
     data() {
-        return {};
+        return {
+            tab1: 0,
+            tab2: 0,
+            visitNum: 0,
+            orderNum: 0,
+            stat1: { visitNum: 0, orderNum: 0 },
+            stat2: {},
+            chartData: null
+        };
+    },
+    computed: {
+        ...mapState(['userInfo'])
+    },
+    created() {
+        this.getData1();
+        this.getData2();
+    },
+    methods: {
+        getData1() {
+            let start, end;
+            switch (this.tab1) {
+                case 0:
+                    start = end = format(new Date(), 'yyyy-MM-dd');
+                    break;
+                case 1:
+                    start = end = format(addDays(new Date(), -1), 'yyyy-MM-dd');
+                    break;
+                case 2:
+                    end = format(new Date(), 'yyyy-MM-dd');
+                    start = format(addDays(new Date(), -6), 'yyyy-MM-dd');
+                    break;
+                case 3:
+                    end = format(new Date(), 'yyyy-MM-dd');
+                    start = format(addDays(new Date(), -29), 'yyyy-MM-dd');
+                    break;
+            }
+            this.$http
+                .get('/visitStat/stat1', {
+                    start,
+                    end
+                })
+                .then(res => {
+                    this.stat1 = res;
+                });
+        },
+        getData2() {
+            let start, end;
+            switch (this.tab2) {
+                case 0:
+                    end = format(addDays(new Date(), -1), 'yyyy-MM-dd');
+                    start = format(addDays(new Date(), -7), 'yyyy-MM-dd');
+                    break;
+                case 1:
+                    end = format(addDays(new Date(), -1), 'yyyy-MM-dd');
+                    start = format(addDays(new Date(), -30), 'yyyy-MM-dd');
+                    break;
+            }
+            this.$http
+                .get('/visitStat/stat2', {
+                    start,
+                    end
+                })
+                .then(res => {
+                    this.stat2 = res;
+
+                    // this.chartData = {
+                    //     labels: res.labels,
+                    //     datasets: [
+                    //         {
+                    //             label: '访问',
+                    //             backgroundColor: gradient,
+                    //             borderColor: '#20a0ff',
+                    //             borderWidth: '1px',
+                    //             data: res.visitNum
+                    //         }
+                    //     ]
+                    // };
+                    this.chartData = {
+                        labels: res.labels,
+                        datasets: [
+                            {
+                                label: '访问',
+                                backgroundColor: 'rgba(255, 99, 132, 0.5)',
+                                borderColor: 'rgb(255, 99, 132)',
+                                borderWidth: 2,
+                                fill: false,
+                                data: res.visitNum
+                            },
+                            {
+                                label: '订单',
+                                backgroundColor: 'rgb(54, 162, 235, 0.5)',
+                                borderColor: 'rgb(54, 162, 235)',
+                                borderWidth: 2,
+                                fill: false,
+                                data: res.orderNum
+                            }
+                        ]
+                    };
+                });
+        }
+    },
+    watch: {
+        tab1() {
+            this.getData1();
+        },
+        tab2() {
+            this.chartData = null;
+            this.getData2();
+        }
     }
 };
 </script>
-<style lang="less" scoped></style>
+<style lang="less" scoped>
+.stat-tabs {
+    margin-top: 15px;
+}
+.stat-title {
+    font-size: 14px;
+    font-weight: bold;
+    margin-top: 10px;
+    text-align: center;
+}
+
+.stat {
+    margin-top: 10px;
+    .flex();
+    .col {
+        .flex-col();
+        flex-basis: 0;
+        flex-grow: 1;
+        align-items: center;
+        .value {
+            color: @text1;
+            font-size: 15px;
+            font-weight: bold;
+            line-height: 26px;
+        }
+        .label {
+            color: @text3;
+            font-size: 13px;
+            line-height: 20px;
+        }
+    }
+}
+</style>

+ 43 - 1
src/main/mobile/yarn.lock

@@ -960,6 +960,13 @@
     "@types/connect" "*"
     "@types/node" "*"
 
+"@types/chart.js@^2.7.55":
+  version "2.9.31"
+  resolved "https://registry.yarnpkg.com/@types/chart.js/-/chart.js-2.9.31.tgz#e8ebc7ed18eb0e5114c69bd46ef8e0037c89d39d"
+  integrity sha512-hzS6phN/kx3jClk3iYqEHNnYIRSi4RZrIGJ8CDLjgatpHoftCezvC44uqB3o3OUm9ftU1m7sHG8+RLyPTlACrA==
+  dependencies:
+    moment "^2.10.2"
+
 "@types/connect-history-api-fallback@*":
   version "1.3.4"
   resolved "https://registry.npm.taobao.org/@types/connect-history-api-fallback/download/@types/connect-history-api-fallback-1.3.4.tgz?cache=0&sync_timestamp=1615910572832&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2F%40types%2Fconnect-history-api-fallback%2Fdownload%2F%40types%2Fconnect-history-api-fallback-1.3.4.tgz#8c0f0e6e5d8252b699f5a662f51bdf82fd9d8bb8"
@@ -2385,6 +2392,29 @@ chardet@^0.7.0:
   resolved "https://registry.npm.taobao.org/chardet/download/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
   integrity sha1-kAlISfCTfy7twkJdDSip5fDLrZ4=
 
+chart.js@^2.9.4:
+  version "2.9.4"
+  resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-2.9.4.tgz#0827f9563faffb2dc5c06562f8eb10337d5b9684"
+  integrity sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==
+  dependencies:
+    chartjs-color "^2.1.0"
+    moment "^2.10.2"
+
+chartjs-color-string@^0.6.0:
+  version "0.6.0"
+  resolved "https://registry.yarnpkg.com/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz#1df096621c0e70720a64f4135ea171d051402f71"
+  integrity sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==
+  dependencies:
+    color-name "^1.0.0"
+
+chartjs-color@^2.1.0:
+  version "2.4.1"
+  resolved "https://registry.yarnpkg.com/chartjs-color/-/chartjs-color-2.4.1.tgz#6118bba202fe1ea79dd7f7c0f9da93467296c3b0"
+  integrity sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==
+  dependencies:
+    chartjs-color-string "^0.6.0"
+    color-convert "^1.9.3"
+
 check-types@^8.0.3:
   version "8.0.3"
   resolved "https://registry.npm.taobao.org/check-types/download/check-types-8.0.3.tgz#3356cca19c889544f2d7a95ed49ce508a0ecf552"
@@ -2579,7 +2609,7 @@ collection-visit@^1.0.0:
     map-visit "^1.0.0"
     object-visit "^1.0.0"
 
-color-convert@^1.9.0, color-convert@^1.9.1:
+color-convert@^1.9.0, color-convert@^1.9.1, color-convert@^1.9.3:
   version "1.9.3"
   resolved "https://registry.npm.taobao.org/color-convert/download/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8"
   integrity sha1-u3GFBpDh8TZWfeYp0tVHHe2kweg=
@@ -5688,6 +5718,11 @@ mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.5, mkdirp@~0.5.1:
   dependencies:
     minimist "^1.2.5"
 
+moment@^2.10.2:
+  version "2.29.1"
+  resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3"
+  integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
+
 move-concurrently@^1.0.1:
   version "1.0.1"
   resolved "https://registry.npm.taobao.org/move-concurrently/download/move-concurrently-1.0.1.tgz#be2c005fda32e0b29af1f05d7c4b33214c701f92"
@@ -8460,6 +8495,13 @@ vue-bus@^1.2.1:
   resolved "https://registry.yarnpkg.com/vue-bus/-/vue-bus-1.2.1.tgz#50577b0b73fc1af9cda8a475fef2f7f0fdad7045"
   integrity sha512-uCSJEWFWoDZz+GV/Pj/wXAC7WVBLD18V62l+2ezd4UCsZWZB27Hz3K0M9WUcbNum/yKBoN+OkOCIrU6A9xqWhw==
 
+vue-chartjs@^3.5.1:
+  version "3.5.1"
+  resolved "https://registry.yarnpkg.com/vue-chartjs/-/vue-chartjs-3.5.1.tgz#d25e845708f7744ae51bed9d23a975f5f8fc6529"
+  integrity sha512-foocQbJ7FtveICxb4EV5QuVpo6d8CmZFmAopBppDIGKY+esJV8IJgwmEW0RexQhxqXaL/E1xNURsgFFYyKzS/g==
+  dependencies:
+    "@types/chart.js" "^2.7.55"
+
 vue-cli-plugin-style-resources-loader@^0.1.5:
   version "0.1.5"
   resolved "https://registry.yarnpkg.com/vue-cli-plugin-style-resources-loader/-/vue-cli-plugin-style-resources-loader-0.1.5.tgz#3e95f4df41f5408e1255664690698c0533648109"