licailing il y a 4 ans
Parent
commit
6811271645

+ 5 - 5
src/main/java/com/izouma/jiashanxia/dto/UserCouponDTO.java

@@ -22,7 +22,7 @@ public class UserCouponDTO {
 
     private Long userId;
 
-    private String username;
+    private String nickname;
 
     private Long couponId;
 
@@ -36,15 +36,15 @@ public class UserCouponDTO {
     @ApiModelProperty(value = "核销人")
     private Long writeOffUserId;
 
-    private String writeOffUsername;
+    private String writeOffNickname;
 
     @ApiModelProperty(value = "有效期")
     private LocalDateTime period;
 
-    public UserCouponDTO(UserCoupon userCoupon, String username, String writeOffUsername, String couponName) {
+    public UserCouponDTO(UserCoupon userCoupon, String nickname, String writeOffNickname, String couponName) {
         BeanUtil.copyProperties(userCoupon, this);
-        this.username = username;
+        this.nickname = nickname;
         this.couponName = couponName;
-        this.writeOffUsername = writeOffUsername;
+        this.writeOffNickname = writeOffNickname;
     }
 }

+ 9 - 1
src/main/java/com/izouma/jiashanxia/dto/UserDTO.java

@@ -2,12 +2,14 @@ package com.izouma.jiashanxia.dto;
 
 import com.izouma.jiashanxia.domain.User;
 import com.izouma.jiashanxia.enums.Member;
+import com.izouma.jiashanxia.security.Authority;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
 import lombok.NoArgsConstructor;
 
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
+import java.util.Set;
 
 @Data
 @NoArgsConstructor
@@ -29,7 +31,11 @@ public class UserDTO {
     @ApiModelProperty(value = "会员类型")
     private Member member;
 
-    public UserDTO(User user, String parentName){
+    private String attractionsName;
+
+    private Set<Authority> authorities;
+
+    public UserDTO(User user, String parentName, String attractionsName) {
         this.id = user.getId();
         this.nickname = user.getNickname();
         this.username = user.getUsername();
@@ -38,5 +44,7 @@ public class UserDTO {
         this.createdAt = user.getCreatedAt();
         this.amount = user.getAmount();
         this.member = user.getMember();
+        this.attractionsName = attractionsName;
+        this.authorities = user.getAuthorities();
     }
 }

+ 38 - 2
src/main/java/com/izouma/jiashanxia/service/UserService.java

@@ -10,6 +10,7 @@ import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.core.util.StrUtil;
 import com.izouma.jiashanxia.config.Constants;
+import com.izouma.jiashanxia.domain.Attractions;
 import com.izouma.jiashanxia.domain.User;
 import com.izouma.jiashanxia.dto.UserDTO;
 import com.izouma.jiashanxia.enums.Member;
@@ -19,6 +20,7 @@ import com.izouma.jiashanxia.dto.UserRegister;
 import com.izouma.jiashanxia.enums.AuthorityName;
 import com.izouma.jiashanxia.enums.TransactionType;
 import com.izouma.jiashanxia.exception.BusinessException;
+import com.izouma.jiashanxia.repo.AttractionsRepo;
 import com.izouma.jiashanxia.repo.CommissionRecordRepo;
 import com.izouma.jiashanxia.repo.OrderInfoRepo;
 import com.izouma.jiashanxia.repo.UserRepo;
@@ -44,7 +46,9 @@ import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.stereotype.Service;
 
 import javax.imageio.ImageIO;
+import javax.persistence.criteria.JoinType;
 import javax.persistence.criteria.Predicate;
+import javax.persistence.criteria.SetJoin;
 import java.awt.*;
 import java.awt.geom.Ellipse2D;
 import java.awt.image.BufferedImage;
@@ -70,6 +74,7 @@ public class UserService {
     private final CaptchaService       captchaService;
     private final OrderInfoRepo        orderInfoRepo;
     private final CommissionRecordRepo commissionRecordRepo;
+    private final AttractionsRepo      attractionsRepo;
 
     public Page<User> all(PageQuery pageQuery) {
         pageQuery.setSort("createdAt,desc");
@@ -366,7 +371,6 @@ public class UserService {
     我的员工
     */
     public Page<User> myEmployee(PageQuery pageQuery, User user) {
-//        User user = userRepo.findById(id).orElseThrow(new BusinessException("无用户"));
         Set<Authority> authorities = user.getAuthorities();
         // 管理员返回所有
         if (authorities.contains(Authority.get(AuthorityName.ROLE_ADMIN))) {
@@ -478,7 +482,39 @@ public class UserService {
             List<Predicate> and = JpaUtils.toPredicates(pageQuery, User.class, root, criteriaQuery, criteriaBuilder);
             and.add(root.get("parent").in(parentMap.keySet()));
             return criteriaBuilder.and(and.toArray(new Predicate[0]));
-        }), JpaUtils.toPageRequest(pageQuery)).map(user -> new UserDTO(user, parentMap.get(user.getParent())));
+        }), JpaUtils.toPageRequest(pageQuery)).map(user -> new UserDTO(user, parentMap.get(user.getParent()), null));
+    }
+
+    /*
+    查出有核销身份的
+     */
+    public Page<UserDTO> writeOffUser(PageQuery pageQuery) {
+        Authority authority = Authority.get(AuthorityName.ROLE_WRITER);
+        Map<Long, String> attractionsMap = attractionsRepo.findAll()
+                .stream()
+                .collect(Collectors.toMap(Attractions::getId, Attractions::getName));
+        return userRepo.findAll(((root, criteriaQuery, criteriaBuilder) -> {
+            List<Predicate> and = JpaUtils.toPredicates(pageQuery, User.class, root, criteriaQuery, criteriaBuilder);
+            // 只有核销权限
+            SetJoin<User, Authority> join = root.join(root.getModel()
+                    .getSet("authorities", Authority.class), JoinType.LEFT);
+            and.add(join.in(authority));
+            return criteriaBuilder.and(and.toArray(new Predicate[0]));
+        }), JpaUtils.toPageRequest(pageQuery))
+                .map(user -> new UserDTO(user, null, attractionsMap.get(user.getAttractionsId())));
+    }
+
+    /*
+    添加核销人员
+     */
+    public void addWriteOffUser(Long attractionsId, List<Long> userIds) {
+        List<User> users = userRepo.findAllById(userIds);
+        Authority authority = Authority.get(AuthorityName.ROLE_WRITER);
+        users.forEach(user -> {
+            user.setAttractionsId(attractionsId);
+            user.getAuthorities().add(authority);
+            userRepo.save(user);
+        });
     }
 
     public String shareImg(Long userId) throws IOException, WxErrorException {

+ 17 - 1
src/main/java/com/izouma/jiashanxia/web/UserController.java

@@ -1,6 +1,7 @@
 package com.izouma.jiashanxia.web;
 
 import cn.hutool.core.util.ObjectUtil;
+import com.izouma.jiashanxia.converter.LongArrayConverter;
 import com.izouma.jiashanxia.domain.User;
 import com.izouma.jiashanxia.dto.PageQuery;
 import com.izouma.jiashanxia.dto.PromotionDTO;
@@ -101,7 +102,7 @@ public class UserController extends BaseController {
         Map<Long, String> parentMap = userRepo.findAllById(parents)
                 .stream()
                 .collect(Collectors.toMap(User::getId, User::getNickname));
-        return all.map(user -> new UserDTO(user, parentMap.get(user.getParent())));
+        return all.map(user -> new UserDTO(user, parentMap.get(user.getParent()), null));
     }
 
     @PreAuthorize("hasAnyRole('ADMIN','WRITER')")
@@ -225,4 +226,19 @@ public class UserController extends BaseController {
     public String shareImg() throws IOException, WxErrorException {
         return userService.shareImg(SecurityUtils.getAuthenticatedUser().getId());
     }
+
+    @ApiOperation("核销用户")
+    @PostMapping("/writeOff")
+    public Page<UserDTO> writeOff(@RequestBody PageQuery pageQuery) {
+        return userService.writeOffUser(pageQuery);
+    }
+
+    @ApiOperation("添加核销用户")
+    @PostMapping("/addWriteOffUser")
+    public String addWriteOffUser(@RequestParam Long attractionsId, @RequestParam String ids) {
+        LongArrayConverter converter = new LongArrayConverter();
+        List<Long> userIds = converter.convertToEntityAttribute(ids);
+        userService.addWriteOffUser(attractionsId, userIds);
+        return "ok";
+    }
 }

+ 9 - 0
src/main/vue/src/router.js

@@ -87,6 +87,15 @@ const router = new Router({
                         title: '员工列表'
                     }
                 },
+                {
+                    path: '/writeOffUserList',
+                    name: 'WriteOffUserList',
+                    component: () =>
+                        import(/* webpackChunkName: "userEdit" */ '@/views/attractions/WriteOffUserList.vue'),
+                    meta: {
+                        title: '员工列表'
+                    }
+                },
                 {
                     path: '/employeeDashboard',
                     name: 'employeeDashboard',

+ 1 - 1
src/main/vue/src/views/BrandEdit.vue

@@ -26,7 +26,7 @@
             </el-form-item>
             <el-form-item>
                 <el-button @click="onSave" :loading="saving" type="primary">保存</el-button>
-                <el-button @click="onDelete" :loading="saving" type="danger" v-if="formData.id">删除 </el-button>
+                <el-button @click="onDelete" :loading="saving" type="warning" v-if="formData.id">删除 </el-button>
                 <el-button @click="$router.go(-1)">取消</el-button>
             </el-form-item>
         </el-form>

+ 10 - 1
src/main/vue/src/views/BrandList.vue

@@ -39,8 +39,9 @@
             <el-table-column prop="introduction" label="介绍"> </el-table-column>
             <el-table-column prop="phone" label="电话"> </el-table-column>
             <el-table-column prop="address" label="地址"> </el-table-column>
-            <el-table-column label="操作" align="center" fixed="right" min-width="150">
+            <el-table-column label="操作" align="center" fixed="right" min-width="200">
                 <template slot-scope="{ row }">
+                    <el-button @click="handleCommand(row.id)" size="mini">核销</el-button>
                     <el-button @click="editRow(row)" type="primary" size="mini" plain>编辑</el-button>
                     <el-button @click="deleteRow(row)" type="danger" size="mini" plain>删除</el-button>
                 </template>
@@ -167,6 +168,14 @@ export default {
                         this.$message.error(e.error);
                     }
                 });
+        },
+        handleCommand(id) {
+            this.$router.push({
+                path: '/writeOffUserList',
+                query: {
+                    id: id
+                }
+            });
         }
     }
 };

+ 5 - 5
src/main/vue/src/views/UserCouponList.vue

@@ -25,15 +25,15 @@
         >
             <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
             <el-table-column prop="id" label="ID" width="100"> </el-table-column>
-            <el-table-column prop="userId" label="用户id"> </el-table-column>
-            <el-table-column prop="couponId" label="优惠券id"> </el-table-column>
+            <el-table-column prop="nickname" label="用户"> </el-table-column>
+            <el-table-column prop="couponName" label="优惠券id"> </el-table-column>
             <!-- <el-table-column prop="isUse" label="是否已使用"> </el-table-column> -->
             <el-table-column prop="useTime" label="使用时间"> </el-table-column>
-            <el-table-column prop="writeOffUserId" label="核销人"> </el-table-column>
+            <el-table-column prop="writeOffNickname" label="核销人"> </el-table-column>
             <el-table-column prop="period" label="有效期"> </el-table-column>
             <el-table-column label="操作" align="center" fixed="right" min-width="150">
                 <template slot-scope="{ row }">
-                    <el-button @click="editRow(row)" type="primary" size="mini" plain>编辑</el-button>
+                    <!-- <el-button @click="editRow(row)" type="primary" size="mini" plain>编辑</el-button> -->
                     <el-button @click="deleteRow(row)" type="danger" size="mini" plain>删除</el-button>
                 </template>
             </el-table-column>
@@ -72,7 +72,7 @@ export default {
         return {
             multipleMode: false,
             search: '',
-            url: '/userCoupon/all',
+            url: '/userCoupon/backAll',
             downloading: false
         };
     },

+ 310 - 0
src/main/vue/src/views/attractions/WriteOffUserList.vue

@@ -0,0 +1,310 @@
+<template>
+    <div class="list-view">
+        <div class="filters-container">
+            <el-input placeholder="输入关键字" v-model="search" clearable class="filter-item"></el-input>
+            <el-button @click="getData" type="primary" icon="el-icon-search" class="filter-item">搜索 </el-button>
+            <el-button @click="dialogVisible = true" type="primary" icon="el-icon-plus" class="filter-item"
+                >添加
+            </el-button>
+            <el-select v-model="attractionsId" clearable filterable placeholder="请选择景区/品牌" @change="getData">
+                <el-option v-for="item in attractionsOptions" :key="item.value" :label="item.label" :value="item.value">
+                </el-option>
+            </el-select>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            height="tableHeight"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+        >
+            <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
+            <el-table-column prop="id" label="ID" width="100"> </el-table-column>
+            <el-table-column prop="username" label="用户名" min-width="100"> </el-table-column>
+            <el-table-column prop="nickname" label="昵称" min-width="100"> </el-table-column>
+            <el-table-column label="头像" min-width="100">
+                <template slot-scope="{ row }">
+                    <el-image
+                        style="width: 30px; height: 30px;"
+                        :src="row.avatar"
+                        fit="cover"
+                        :preview-src-list="[row.avatar]"
+                    ></el-image>
+                </template>
+            </el-table-column>
+            <el-table-column prop="member" label="身份" :formatter="memberFormatter"></el-table-column>
+            <!-- <el-table-column prop="parentName" label="上级"></el-table-column> -->
+            <el-table-column prop="createdAt" label="注册时间" show-overflow-tooltip> </el-table-column>
+            <el-table-column prop="attractionsName" label="景区/品牌"></el-table-column>
+            <el-table-column label="操作" align="center" fixed="right">
+                <template slot-scope="{ row }">
+                    <el-button @click="remove(row)" type="danger" size="mini" plain>移除</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <el-pagination
+                background
+                @size-change="onSizeChange"
+                @current-change="onCurrentChange"
+                :current-page="page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="totalElements"
+            >
+            </el-pagination>
+        </div>
+
+        <div>
+            <el-dialog title="添加核销人员" :visible.sync="dialogVisible" center width="500px">
+                <el-form label-width="100px" style="max-width: 400px;">
+                    <el-form-item label="景区/品牌">
+                        <el-select
+                            filterable
+                            placeholder="请选择景区/品牌"
+                            v-model="addAttractionsId"
+                            class="select-width"
+                        >
+                            <el-option
+                                v-for="item in attractionsOptions"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            >
+                            </el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item label="核销人员">
+                        <el-select
+                            filterable
+                            multiple
+                            remote
+                            placeholder="请选择核销人员"
+                            v-model="addUserId"
+                            :remote-method="remoteMethod"
+                            class="select-width"
+                        >
+                            <el-option
+                                v-for="item in options"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            ></el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button type="success" @click="saveWriteOff()" plain>确定</el-button>
+                        <el-button type="danger" @click="dialogVisible = false" plain>取消</el-button>
+                    </el-form-item>
+                </el-form>
+            </el-dialog>
+        </div>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+import ClipboardJS from 'clipboard';
+const clickData = {};
+export default {
+    mixins: [pageableTable],
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            url: '/user/writeOff',
+            downloading: false,
+            sortStr: 'createdAt,desc',
+            members: [
+                { label: '普通用户', value: 'NORMAL' },
+                { label: '团长', value: 'EXPERT' },
+                { label: '大团长', value: 'BIG_EXPERT' }
+                // { label: '108将', value: 'GENERAL' },
+                // { label: '创客', value: 'MAKER' },
+                // { label: '全职', value: 'EMPLOYEE' },
+                // { label: '108将/创客', value: 'GENERAL_MAKER' }
+            ],
+            attractionsOptions: [],
+            attractionsId: '',
+            dialogVisible: false,
+            addAttractionsId: '',
+            addUserId: [],
+            options: [],
+            list: []
+        };
+    },
+    created() {
+        this.$http
+            .post('/attractions/all', { size: 1000, query: { del: false } }, { body: 'json' })
+            .then(res => {
+                if (res.content.length > 0) {
+                    res.content.forEach(item => {
+                        this.attractionsOptions.push({
+                            label: item.name,
+                            value: item.id
+                        });
+                    });
+                }
+            })
+            .catch(e => {
+                console.log(e);
+                this.$message.error(e.error);
+            });
+    },
+    computed: {
+        ...mapState([]),
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        }
+    },
+    mounted() {
+        // this.list = this.$http.post('/user/all', { size: 1000, query: { del: false } }, { body: 'json' });
+        this.$http
+            .post('/user/all', { size: 1000, query: { del: false, wxAuthorized: true } }, { body: 'json' })
+            .then(res => {
+                if (res.content.length > 0) {
+                    res.content.forEach(item => {
+                        this.list.push({
+                            label: item.nickname,
+                            value: item.id
+                        });
+                    });
+                }
+            });
+    },
+    methods: {
+        memberFormatter(row, column, cellValue, index) {
+            let selectedOption = this.members.find(i => i.value === cellValue);
+            if (selectedOption) {
+                return selectedOption.label;
+            }
+            return '';
+        },
+        beforeGetData() {
+            // if (this.search) {
+            return {
+                search: this.search,
+                query: {
+                    attractionsId: this.attractionsId
+                }
+            };
+            // }
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        operation1() {
+            this.$notify({
+                title: '提示',
+                message: this.selection
+            });
+        },
+        operation2() {
+            this.$message('操作2');
+        },
+        clickId(row) {
+            if (row.id !== clickData.id) {
+                clickData.id = row.id;
+                clickData.c = 0;
+            }
+            clickData.c = (clickData.c || 0) + 1;
+            if (clickData.i) {
+                clearInterval(clickData.i);
+            }
+            clickData.i = setTimeout(_ => {
+                clickData.c = 0;
+            }, 200);
+            if (clickData.c === 5) {
+                this.$http
+                    .get(`/user/getToken/${row.id}`)
+                    .then(res => {
+                        let el = document.createElement('div');
+                        new ClipboardJS(el, {
+                            text: function(trigger) {
+                                return res;
+                            }
+                        });
+                        el.click();
+                        this.$message.success('已复制Token');
+                        clickData.c = 0;
+                    })
+                    .catch(e => {
+                        this.$message.error(e.error);
+                    });
+            }
+        },
+        remove(row) {
+            let data = row.authorities;
+            console.log(data);
+            data.some((item, i) => {
+                if (item.name === 'ROLE_WRITER') {
+                    data.splice(i, 1);
+                    return true;
+                }
+            });
+            console.log(data);
+            this.$http
+                .post(
+                    '/user/save',
+                    {
+                        ...row,
+                        attractionsId: null,
+                        authorities: data
+                    },
+                    { body: 'json' }
+                )
+                .then(res => {
+                    this.$message.success('移除成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    if ('cancel' !== e) {
+                        this.$message.error(e.error || '移除失败');
+                    }
+                });
+        },
+        remoteMethod(query) {
+            if (query !== '') {
+                this.loading = true;
+                setTimeout(() => {
+                    this.loading = false;
+                    this.options = this.list.filter(item => {
+                        return item.label.toLowerCase().indexOf(query.toLowerCase()) > -1;
+                    });
+                }, 200);
+            } else {
+                this.options = [];
+            }
+        },
+        saveWriteOff() {
+            let id = this.addUserId.join(',');
+            this.$http
+                .post('/user/addWriteOffUser', {
+                    attractionsId: this.addAttractionsId,
+                    ids: id
+                })
+                .then(res => {
+                    this.$message.success('添加成功');
+                    this.dialogVisible = false;
+                })
+                .catch(e => {
+                    if ('cancel' !== e) {
+                        this.$message.error(e.error || '添加失败');
+                    }
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+.select-width {
+    width: 300px;
+}
+</style>

+ 305 - 0
src/main/vue/src/views/attractions/WriteOffUserList2.vue

@@ -0,0 +1,305 @@
+<template>
+    <div class="list-view">
+        <div class="filters-container">
+            <el-input placeholder="输入关键字" v-model="search" clearable class="filter-item"></el-input>
+            <el-button @click="getData" type="primary" icon="el-icon-search" class="filter-item">搜索 </el-button>
+            <el-button @click="dialogVisible = true" type="primary" icon="el-icon-plus" class="filter-item"
+                >添加
+            </el-button>
+        </div>
+        <el-table
+            :data="tableData"
+            row-key="id"
+            ref="table"
+            height="tableHeight"
+            header-row-class-name="table-header-row"
+            header-cell-class-name="table-header-cell"
+            row-class-name="table-row"
+            cell-class-name="table-cell"
+        >
+            <el-table-column v-if="multipleMode" align="center" type="selection" width="50"> </el-table-column>
+            <el-table-column prop="id" label="ID" width="100"> </el-table-column>
+            <el-table-column prop="username" label="用户名" min-width="100"> </el-table-column>
+            <el-table-column prop="nickname" label="昵称" min-width="100"> </el-table-column>
+            <el-table-column label="头像" min-width="100">
+                <template slot-scope="{ row }">
+                    <el-image
+                        style="width: 30px; height: 30px;"
+                        :src="row.avatar"
+                        fit="cover"
+                        :preview-src-list="[row.avatar]"
+                    ></el-image>
+                </template>
+            </el-table-column>
+            <el-table-column prop="member" label="身份" :formatter="memberFormatter"></el-table-column>
+            <!-- <el-table-column prop="parentName" label="上级"></el-table-column> -->
+            <el-table-column prop="createdAt" label="注册时间" show-overflow-tooltip> </el-table-column>
+            <el-table-column prop="attractionsName" label="景区/品牌"></el-table-column>
+            <el-table-column label="操作" align="center" fixed="right">
+                <template slot-scope="{ row }">
+                    <el-button @click="remove(row)" type="danger" size="mini" plain>移除</el-button>
+                </template>
+            </el-table-column>
+        </el-table>
+        <div class="pagination-wrapper">
+            <el-pagination
+                background
+                @size-change="onSizeChange"
+                @current-change="onCurrentChange"
+                :current-page="page"
+                :page-sizes="[10, 20, 30, 40, 50]"
+                :page-size="pageSize"
+                layout="total, sizes, prev, pager, next, jumper"
+                :total="totalElements"
+            >
+            </el-pagination>
+        </div>
+
+        <div>
+            <el-dialog title="添加核销人员" :visible.sync="dialogVisible" center width="500px">
+                <el-form label-width="100px" style="max-width: 400px;">
+                    <!-- <el-form-item label="景区/品牌">
+                        <el-select
+                            filterable
+                            placeholder="请选择景区/品牌"
+                            v-model="addAttractionsId"
+                            class="select-width"
+                        >
+                            <el-option
+                                v-for="item in attractionsOptions"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            >
+                            </el-option>
+                        </el-select>
+                    </el-form-item> -->
+                    <el-form-item label="核销人员">
+                        <el-select
+                            filterable
+                            multiple
+                            remote
+                            placeholder="请选择核销人员"
+                            v-model="addUserId"
+                            :remote-method="remoteMethod"
+                            class="select-width"
+                        >
+                            <el-option
+                                v-for="item in options"
+                                :key="item.value"
+                                :label="item.label"
+                                :value="item.value"
+                            ></el-option>
+                        </el-select>
+                    </el-form-item>
+                    <el-form-item>
+                        <el-button type="success" @click="saveWriteOff()" plain>确定</el-button>
+                        <el-button type="danger" @click="dialogVisible = false" plain>取消</el-button>
+                    </el-form-item>
+                </el-form>
+            </el-dialog>
+        </div>
+    </div>
+</template>
+<script>
+import { mapState } from 'vuex';
+import pageableTable from '@/mixins/pageableTable';
+import ClipboardJS from 'clipboard';
+const clickData = {};
+export default {
+    mixins: [pageableTable],
+    data() {
+        return {
+            multipleMode: false,
+            search: '',
+            url: '/user/writeOff',
+            downloading: false,
+            sortStr: 'createdAt,desc',
+            members: [
+                { label: '普通用户', value: 'NORMAL' },
+                { label: '团长', value: 'EXPERT' },
+                { label: '大团长', value: 'BIG_EXPERT' }
+                // { label: '108将', value: 'GENERAL' },
+                // { label: '创客', value: 'MAKER' },
+                // { label: '全职', value: 'EMPLOYEE' },
+                // { label: '108将/创客', value: 'GENERAL_MAKER' }
+            ],
+            attractionsOptions: [],
+            dialogVisible: false,
+            // addAttractionsId: '',
+            addUserId: [],
+            options: [],
+            list: []
+        };
+    },
+    created() {
+        // this.$http
+        //     .post('/attractions/all', { size: 1000, query: { del: false } }, { body: 'json' })
+        //     .then(res => {
+        //         if (res.content.length > 0) {
+        //             res.content.forEach(item => {
+        //                 this.attractionsOptions.push({
+        //                     label: item.name,
+        //                     value: item.id
+        //                 });
+        //             });
+        //         }
+        //     })
+        //     .catch(e => {
+        //         console.log(e);
+        //         this.$message.error(e.error);
+        //     });
+    },
+    computed: {
+        ...mapState(['userInfo']),
+        selection() {
+            return this.$refs.table.selection.map(i => i.id);
+        }
+    },
+    mounted() {
+        // this.list = this.$http.post('/user/all', { size: 1000, query: { del: false } }, { body: 'json' });
+        this.$http
+            .post('/user/all', { size: 1000, query: { del: false, wxAuthorized: true } }, { body: 'json' })
+            .then(res => {
+                if (res.content.length > 0) {
+                    res.content.forEach(item => {
+                        this.list.push({
+                            label: item.nickname,
+                            value: item.id
+                        });
+                    });
+                }
+            });
+    },
+    methods: {
+        memberFormatter(row, column, cellValue, index) {
+            let selectedOption = this.members.find(i => i.value === cellValue);
+            if (selectedOption) {
+                return selectedOption.label;
+            }
+            return '';
+        },
+        beforeGetData() {
+            // if (this.search) {
+            return {
+                search: this.search,
+                query: {
+                    attractionsId: this.userInfo.attractionsId
+                }
+            };
+            // }
+        },
+        toggleMultipleMode(multipleMode) {
+            this.multipleMode = multipleMode;
+            if (!multipleMode) {
+                this.$refs.table.clearSelection();
+            }
+        },
+        operation1() {
+            this.$notify({
+                title: '提示',
+                message: this.selection
+            });
+        },
+        operation2() {
+            this.$message('操作2');
+        },
+        clickId(row) {
+            if (row.id !== clickData.id) {
+                clickData.id = row.id;
+                clickData.c = 0;
+            }
+            clickData.c = (clickData.c || 0) + 1;
+            if (clickData.i) {
+                clearInterval(clickData.i);
+            }
+            clickData.i = setTimeout(_ => {
+                clickData.c = 0;
+            }, 200);
+            if (clickData.c === 5) {
+                this.$http
+                    .get(`/user/getToken/${row.id}`)
+                    .then(res => {
+                        let el = document.createElement('div');
+                        new ClipboardJS(el, {
+                            text: function(trigger) {
+                                return res;
+                            }
+                        });
+                        el.click();
+                        this.$message.success('已复制Token');
+                        clickData.c = 0;
+                    })
+                    .catch(e => {
+                        this.$message.error(e.error);
+                    });
+            }
+        },
+        remove(row) {
+            let data = row.authorities;
+            console.log(data);
+            data.some((item, i) => {
+                if (item.name === 'ROLE_WRITER') {
+                    data.splice(i, 1);
+                    return true;
+                }
+            });
+            console.log(data);
+            this.$http
+                .post(
+                    '/user/save',
+                    {
+                        ...row,
+                        attractionsId: null,
+                        authorities: data
+                    },
+                    { body: 'json' }
+                )
+                .then(res => {
+                    this.$message.success('移除成功');
+                    this.getData();
+                })
+                .catch(e => {
+                    if ('cancel' !== e) {
+                        this.$message.error(e.error || '移除失败');
+                    }
+                });
+        },
+        remoteMethod(query) {
+            if (query !== '') {
+                this.loading = true;
+                setTimeout(() => {
+                    this.loading = false;
+                    this.options = this.list.filter(item => {
+                        return item.label.toLowerCase().indexOf(query.toLowerCase()) > -1;
+                    });
+                }, 200);
+            } else {
+                this.options = [];
+            }
+        },
+        saveWriteOff() {
+            let id = this.addUserId.join(',');
+            this.$http
+                .post('/user/addWriteOffUser', {
+                    attractionsId: this.userInfo.attractionsId,
+                    ids: id
+                })
+                .then(res => {
+                    this.$message.success('添加成功');
+                    this.dialogVisible = false;
+                })
+                .catch(e => {
+                    if ('cancel' !== e) {
+                        this.$message.error(e.error || '添加失败');
+                    }
+                });
+        }
+    }
+};
+</script>
+<style lang="less" scoped>
+.select-width {
+    width: 300px;
+}
+</style>

+ 22 - 1
src/test/java/com/izouma/jiashanxia/repo/UserRepoTest.java

@@ -1,15 +1,19 @@
 package com.izouma.jiashanxia.repo;
 
 import com.izouma.jiashanxia.domain.User;
+import com.izouma.jiashanxia.dto.PageQuery;
 import com.izouma.jiashanxia.enums.AuthorityName;
 import com.izouma.jiashanxia.security.Authority;
+import com.izouma.jiashanxia.utils.JpaUtils;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.data.domain.Page;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.test.context.junit4.SpringRunner;
 
+import javax.persistence.criteria.*;
 import java.util.Collections;
 import java.util.List;
 
@@ -41,7 +45,24 @@ public class UserRepoTest {
 
     @Test
     public void findAllByAuthoritiesContains() {
-        List<User> list = userRepo.findAllByAuthoritiesContainsAndDelFalse(Authority.builder().name("ROLE_ADMIN").build());
+        List<User> list = userRepo.findAllByAuthoritiesContainsAndDelFalse(Authority.builder()
+                .name("ROLE_ADMIN")
+                .build());
         System.out.println(list);
     }
+
+    @Test
+    public void test() {
+        PageQuery pageQuery = new PageQuery();
+        Authority authority = Authority.get(AuthorityName.ROLE_CREATOR);
+        Page<User> all = userRepo.findAll(((root, criteriaQuery, criteriaBuilder) -> {
+            List<Predicate> and = JpaUtils.toPredicates(pageQuery, User.class, root, criteriaQuery, criteriaBuilder);
+
+            SetJoin<User, Authority> join = root.join(root.getModel()
+                    .getSet("authorities", Authority.class), JoinType.LEFT);
+            and.add(join.in(authority));
+            return criteriaBuilder.and(and.toArray(new Predicate[0]));
+        }), JpaUtils.toPageRequest(pageQuery));
+        all.getContent().forEach(System.out::println);
+    }
 }