wxxwjef 2 سال پیش
والد
کامیت
9e3f0c440a

+ 5 - 0
api/src/main/java/com/wx/application/adapter/controller/GorseController.java

@@ -121,6 +121,11 @@ public class GorseController extends BaseController {
         return success(gorseService.getRecommendByUser(gQo.getUserId(), gQo.getRecommendation(), gQo.getCategory(), gQo.getN()));
     }
 
+    @GetMapping(value = "/get_feedback_list/{userId}/{type}")
+    public ResponseData getFeedbackList(@PathVariable("userId") String userId, @PathVariable("type") String type) throws Exception {
+        return success(gorseService.listFeedback(userId, type));
+    }
+
     @PutMapping(value = "/bulk/{type}")
     public ResponseData bulkUserOrItem(@RequestParam Map<String, String> data, @RequestParam("file") MultipartFile file, @PathVariable("type") String type) throws Exception {
         return success(gorseService.bulkUserOrItem(data, file, type));

+ 8 - 1
api/src/main/java/com/wx/application/gorse4j/Feedback.java

@@ -12,15 +12,17 @@ public class Feedback {
     private String userId;
     private String itemId;
     private String timestamp;
+    private Item item;
 
     public Feedback() {
     }
 
-    public Feedback(String feedbackType, String userId, String itemId, String timestamp) {
+    public Feedback(String feedbackType, String userId, String itemId, String timestamp, Item item) {
         this.feedbackType = feedbackType;
         this.userId = userId;
         this.itemId = itemId;
         this.timestamp = timestamp;
+        this.item = item;
     }
 
     @JsonProperty("FeedbackType")
@@ -38,6 +40,11 @@ public class Feedback {
         return itemId;
     }
 
+    @JsonProperty("Item")
+    public Item getItem() {
+        return item;
+    }
+
     @JsonProperty("Timestamp")
     public String getTimestamp() {
         return timestamp;

+ 1 - 1
api/src/main/java/com/wx/application/gorse4j/GorseService.java

@@ -70,7 +70,7 @@ public class GorseService {
     }
 
     public List<Feedback> listFeedback(String userId, String feedbackType) throws IOException {
-        return Arrays.asList(this.request("GET", this.endpoint + "/api/user/" + userId + "/feedback/" + feedbackType, null, Feedback[].class));
+        return Arrays.asList(this.request("GET", this.endpoint + "/api/dashboard/user/" + userId + "/feedback/" + feedbackType, null, Feedback[].class));
     }
 
     public List<String> getRecommend(String userId) throws IOException {

+ 12 - 1
web/src/components/menus/UserManage.vue

@@ -40,11 +40,14 @@
           </el-table-column>
           <el-table-column label="操作" width="240">
             <template slot-scope="scope">
+              <el-button @click.native.prevent="feedbackHistory(scope.row)" type="text" size="small">
+                反馈历史
+              </el-button>
               <el-button @click.native.prevent="linkUser(scope.row)" type="text" size="small">
                 关注用户
               </el-button>
               <el-button @click.native.prevent="similarUser(scope.row)" type="text" size="small">
-                相似
+                相似用户
               </el-button>
               <el-button @click.native.prevent="recommendItem(scope.row)" type="text" size="small">
                 查看推荐
@@ -181,6 +184,14 @@ export default {
     downloadUser() {
       window.open(getBaseUrl() + '/bulk/get_bulk/users');
     },
+    feedbackHistory(item) {
+      _this.$router.push({
+        path: "feedbackHistory", query: {
+          userId: item.fid,
+          cursorArr: JSON.stringify(_this.cursorArr)
+        }
+      });
+    },
     linkUser(item) {
       _this.$router.push({
         path: "linkUser", query: {

+ 263 - 0
web/src/components/menus/feedback/ImportFeedback.vue

@@ -0,0 +1,263 @@
+<template>
+  <div>
+    <div class="menu-title">
+      导入反馈
+      <div class="icon icon-back" @click="backBtn">
+        <i title="返回"></i>
+      </div>
+    </div>
+    <div class="block_box" style="margin-top: 20px;padding: 20px">
+      <el-form ref="cmd" label-width="100px" :rules="rules" :model="cmd" :inline="true">
+        <el-form-item label="文件" prop="file">
+          <el-upload
+              class="upload-demo"
+              action="#"
+              :auto-upload="false"
+              accept=".csv"
+              :on-change="handleFileChange"
+              :limit="1"
+              :show-file-list="false">
+            <el-button size="small" type="primary">点击上传</el-button>
+            <div slot="tip" class="el-upload__tip">只能上传csv文件</div>
+          </el-upload>
+        </el-form-item>
+        <br/>
+        <el-form-item label="字段分隔符" prop="field">
+          <el-input v-model="cmd.field" placeholder="请输入字段分隔符" style="width: 300px"
+                    @change="handleFieldChange"></el-input>
+        </el-form-item>
+        <br/>
+        <el-form-item label="反馈类型" prop="labels">
+          <el-select v-model="cmd.feedbackType" style="width: 300px" @change="refreshList">
+            <template v-if="cmd.ifFirstHead">
+              <el-option :value="index" :label="item" v-for="(item,index) in columnList" :key="index"></el-option>
+            </template>
+            <template v-if="!cmd.ifFirstHead">
+              <el-option :value="index" :label="index" v-for="(item,index) in columnList" :key="index"></el-option>
+            </template>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="用户ID" prop="labels">
+          <el-select v-model="cmd.userId" style="width: 300px" @change="refreshList">
+            <template v-if="cmd.ifFirstHead">
+              <el-option :value="index" :label="item" v-for="(item,index) in columnList" :key="index"></el-option>
+            </template>
+            <template v-if="!cmd.ifFirstHead">
+              <el-option :value="index" :label="index" v-for="(item,index) in columnList" :key="index"></el-option>
+            </template>
+          </el-select>
+        </el-form-item>
+        <br/>
+        <el-form-item label="物类ID" prop="labels">
+          <el-select v-model="cmd.itemId" style="width: 300px" @change="refreshList">
+            <template v-if="cmd.ifFirstHead">
+              <el-option :value="index" :label="item" v-for="(item,index) in columnList" :key="index"></el-option>
+            </template>
+            <template v-if="!cmd.ifFirstHead">
+              <el-option :value="index" :label="index" v-for="(item,index) in columnList" :key="index"></el-option>
+            </template>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="日期">
+          <el-select v-model="cmd.timestamp" style="width: 300px" @change="refreshList">
+            <template v-if="cmd.ifFirstHead">
+              <el-option :value="index" :label="item" v-for="(item,index) in columnList" :key="index"></el-option>
+            </template>
+            <template v-if="!cmd.ifFirstHead">
+              <el-option :value="index" :label="index" v-for="(item,index) in columnList" :key="index"></el-option>
+            </template>
+          </el-select>
+        </el-form-item>
+        <br/>
+        <el-form-item label=" ">
+          <el-checkbox label="第一行为表头" v-model="cmd.ifFirstHead" @change="refreshList"></el-checkbox>
+        </el-form-item>
+      </el-form>
+      <el-table :data="list" style="width: 100%" height="400" v-show="list.length > 0">
+        <el-table-column type="index" label="行号" width="60"></el-table-column>
+        <el-table-column prop="feedbackType" :label="'反馈类型('+(cmd.ifFirstHead ? columnList[cmd.feedbackType] : cmd.feedbackType)+')'"></el-table-column>
+        <el-table-column prop="userId" :label="'用户ID('+(cmd.ifFirstHead ? columnList[cmd.userId] : cmd.userId)+')'"></el-table-column>
+        <el-table-column prop="itemId" :label="'物类ID('+(cmd.ifFirstHead ? columnList[cmd.itemId] : cmd.itemId)+')'"></el-table-column>
+        <el-table-column prop="timestamp"
+                         :label="'日期('+(cmd.ifFirstHead ? columnList[cmd.timestamp] : cmd.timestamp)+')'"></el-table-column>
+      </el-table>
+      <el-button type="primary" class="submit-btn" @click="confirmSubmit" :disabled="list.length === 0">确认提交
+      </el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import request from '@/utils/request';
+
+var _this;
+export default {
+  name: "ImportUser",
+  data() {
+    return {
+      config: /* Papa Parse config object */ {
+        delimiter: "", // auto-detect
+        newline: "", // auto-detect
+        quoteChar: '"',
+        escapeChar: '"',
+        header: true,
+        dynamicTyping: true,
+        preview: 0,
+        encoding: "",
+        delimitersToGuess: [',']
+        // ?? callback function ??
+      },
+      cmd: {
+        field: ',',
+        userId: '',
+        itemId: '',
+        timestamp: '',
+        feedbackType: '',
+        ifFirstHead: true
+      },
+      file: [],
+      originList: [], // 原始的可能包含表头的数据,默认存储最多21行(如果数据不止21行的话)
+      list: [], // 页面table显示的数据
+      columnList: [],// 上传csv的表头
+      rules: {
+        fid: [
+          {required: true, message: '请选择文件'}
+        ],
+        field: [
+          {required: true, message: '字段分隔符不能为空'}
+        ],
+        categories: [
+          {required: true, message: '类别分隔符不能为空'}
+        ],
+      },
+    }
+  },
+  mounted() {
+    _this = this;
+  },
+  methods: {
+    handleFileChange(file, fileList) {
+      _this.file = file.raw;
+      if(_this.cmd.field.length > 0) {
+        _this.handleFieldChange();
+      }
+    },
+    handleFieldChange() {
+      this.$papa.parse(_this.file, {
+        delimiter: _this.cmd.field,
+        complete: (results) => {
+          _this.originList = results.data.slice(0, 21);
+          // 提前填充一个默认的值给对应的ID列和标签列
+          _this.prepareFieldAndLabels();
+          _this.refreshList();
+        }
+      })
+    },
+    prepareFieldAndLabels() {
+      _this.columnList = _this.originList[0];
+      _this.cmd.feedbackType = 0;
+      _this.cmd.userId = 1;
+      _this.cmd.itemId = 2;
+      _this.cmd.timestamp = 3;
+    },
+    refreshList() {
+      // 处理原始数据,将行文本截取成数组放入list
+      _this.list = [];
+      if (_this.cmd.field.length === 0) {
+        return;
+      }
+
+      for (let i in _this.originList) {
+        if (_this.cmd.ifFirstHead && i == 0) {
+          // 第一行是表头,跳过list塞入
+          continue;
+        }
+        let row = _this.originList[i];
+        let errorFlag = false;
+
+        _this.list.push({
+          userId: row[_this.cmd.userId],
+          itemId: row[_this.cmd.itemId],
+          feedbackType: row[_this.cmd.feedbackType],
+          timestamp: row[_this.cmd.timestamp],
+        });
+      }
+    },
+    confirmSubmit() { // 提交确认
+      this.$refs.cmd.validate((valid) => {
+        let formData = new FormData();
+        formData.append("sep", _this.cmd.field);
+        formData.append("has-header", _this.cmd.ifFirstHead);
+        formData.append("file", _this.file);
+        let userIdIndex = _this.cmd.userId;
+        let itemIdIndex = _this.cmd.itemId;
+        let feedbackTypeIndex = _this.cmd.feedbackType;
+        let timestampIndex = _this.cmd.timestamp;
+        let _format = '';
+        for (let i = 0; i <= Math.max(userIdIndex, itemIdIndex, feedbackTypeIndex, timestampIndex); i++) {
+          if (i == userIdIndex) {
+            _format += "u";
+          } else if (i == itemIdIndex) {
+            _format += "i";
+          } else if (i == timestampIndex) {
+            _format += "t";
+          } else if (i == feedbackTypeIndex) {
+            _format += "f";
+          } else {
+            _format += "_";
+          }
+        }
+        formData.append("format", _format);
+        request({
+          url: '/gorse/bulk/feedback',
+          method: 'put',
+          data: formData
+        }).then(res => {
+          if(res.data && res.data.RowAffected) {
+            _this.$message.success(`成功导入${res.data.RowAffected}条记录`);
+          } else {
+            _this.$message.warning("导入异常");
+          }
+        });
+      });
+    },
+    backBtn() {
+      this.$router.push({
+        path: "feedbackType", query: {
+          qo: JSON.stringify({
+            pageNo: 1,
+            pageSize: 10,
+            LIKES_fid: '',
+          })
+        }
+      });
+    },
+
+  }
+}
+</script>
+<style src="../../../css/back.css" scoped></style>
+<style scoped lang="scss">
+
+.block_box {
+  margin: 25px;
+  background: #FFFFFF;
+  border-radius: 3px;
+
+  .title {
+    border-bottom: 1px solid #ccc;
+    padding: 15px 25px;
+  }
+
+  > div {
+    padding: 25px;
+
+  }
+
+  .submit-btn {
+    margin-left: auto;
+    margin-top: 20px;
+    display: flex;
+  }
+}
+</style>

+ 148 - 0
web/src/components/menus/item/FeedbackHistory.vue

@@ -0,0 +1,148 @@
+<template>
+  <div style="height: 100%">
+    <div class="menu-title">
+      正向反馈历史({{ gorseQo.userId }})
+      <div class="icon icon-back" @click="backBtn">
+        <i title="返回"></i>
+      </div>
+    </div>
+    <div class="block_box">
+      <div class="line-item-box">
+        <div class="line-item" v-for="(item,index) in list" :key="index">
+          <div class="title-time">
+            <div>{{ item.ItemId }}</div>
+            <div>{{ item.Timestamp }}</div>
+          </div>
+          <div class="description">{{ item.Comment }}</div>
+          <div class="labels">
+            <el-tag v-for="la in item.Labels">{{ la }}</el-tag>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import request from '@/utils/request';
+
+var _this;
+export default {
+  name: "RecommendItem",
+  data() {
+    return {
+      qo: "",
+      gorseQo: {
+        userId: '',
+        n: 10,
+        recommendation: '_',
+        category: ''
+      },
+      user: {},
+      list: []
+    }
+  },
+  mounted() {
+    _this = this;
+    _this.qo = _this.$route.query.qo;
+    _this.gorseQo.userId = _this.$route.query.userId;
+    _this.getFeedbackList();
+  },
+  methods: {
+    getFeedbackList() {
+      request({
+        url: '/gorse/get_feedback_list/' + _this.gorseQo.userId + "/like",
+        method: 'get'
+      }).then(res => {
+        res.data.forEach(row => {
+          row.Labels = row.Item.Labels;
+          row.ItemId = row.Item.ItemId;
+          row.Comment = row.Item.Comment;
+          if(_this.list.length < 20) {
+            _this.list.push(row);
+          }
+        });
+      });
+      request({
+        url: '/gorse/get_feedback_list/' + _this.gorseQo.userId + "/star",
+        method: 'get'
+      }).then(res => {
+        res.data.forEach(row => {
+          row.Labels = row.Item.Labels;
+          row.ItemId = row.Item.ItemId;
+          row.Comment = row.Item.Comment;
+          if(_this.list.length < 20) {
+            _this.list.push(row);
+          }
+        });
+      });
+    },
+    backBtn() {
+      _this.$router.back();
+    },
+  }
+}
+</script>
+<style src="../../../css/back.css" scoped></style>
+<style scoped lang="scss">
+
+.block_box {
+  margin: 25px 25px 0 25px;
+  background: #FFFFFF;
+  border-radius: 3px;
+  height: calc(100% - 120px);
+
+  .title {
+    border-bottom: 1px solid #ccc;
+    padding: 15px 25px;
+  }
+
+}
+
+.demo-form-inline {
+  padding: 25px 25px 0 25px;
+}
+
+.line-item-box {
+  height: calc(100% - 40px);
+  overflow-y: auto;
+  width: 100%;
+
+  .line-item {
+    font-size: 14px;
+    line-height: 32px;
+    padding: 0 25px 10px 25px;
+    margin: 20px 0;
+    border-bottom: 1px solid #dee0e2;
+
+
+    .title-time {
+      display: flex;
+
+      div:first-child {
+        font-weight: bold;
+        font-size: 18px;
+        color: #9da2a8;
+      }
+
+      div:last-child {
+        margin-left: auto;
+        color: #bbbcbd;
+      }
+    }
+
+    .description {
+      color: #bbbcbd;
+    }
+
+    .labels {
+      color: #9da2a8;
+
+      .el-tag {
+        margin: 0 10px 10px 0;
+      }
+    }
+  }
+
+}
+</style>

+ 1 - 2
web/src/components/menus/item/RecommendItem.vue

@@ -7,7 +7,6 @@
       </div>
     </div>
     <div class="block_box">
-<!--      <div class="title">推荐</div>-->
       <el-form :inline="true" class="demo-form-inline">
         <el-form-item>
           <el-select v-model="gorseQo.recommendation" @change="getRecommendItem">
@@ -31,7 +30,7 @@
           </div>
           <div class="description">{{ item.Comment }}</div>
           <div class="labels">
-            <el-tag v-for="la in item.Labels">{{ la }}</el-tag>
+            <el-tag v-for="(la,index) in item.Labels" :key="index">{{ la }}</el-tag>
           </div>
         </div>
       </div>

+ 4 - 0
web/src/router/index.js

@@ -88,6 +88,10 @@ export default new Router({
                     component: () => import('@/components/menus/item/RecommendItem')
                 },
                 {
+                    path: 'feedbackHistory',
+                    component: () => import('@/components/menus/item/FeedbackHistory')
+                },
+                {
                     path: 'similarItem',
                     component: () => import('@/components/menus/item/SimilarItem')
                 },