新增角色管理

1.x
tanghc 5 years ago
parent 36133adf62
commit 21792d39b1
  1. 46
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/isv/RoleApi.java
  2. 24
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/isv/param/RoleForm.java
  3. 19
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/isv/param/RolePageParam.java
  4. 8
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/api/isv/result/RoleVO.java
  5. 7
      sop-admin/sop-admin-server/src/main/java/com/gitee/sop/adminserver/entity/IsvInfo.java
  6. 2
      sop-admin/sop-admin-server/src/main/resources/public/index.html
  7. 2
      sop-admin/sop-admin-server/src/main/resources/public/static/js/app.61234abc.js
  8. 2
      sop-admin/sop-admin-server/src/main/resources/public/static/js/chunk-25908fca.e7e4b6d5.js
  9. 1
      sop-admin/sop-admin-server/src/main/resources/public/static/js/chunk-2d2085ef.a63a74dc.js
  10. 8
      sop-admin/sop-admin-vue/src/router/index.js
  11. 2
      sop-admin/sop-admin-vue/src/views/isv/index.vue
  12. 179
      sop-admin/sop-admin-vue/src/views/isv/role.vue
  13. 2
      sop.sql

@ -3,14 +3,20 @@ package com.gitee.sop.adminserver.api.isv;
import com.gitee.easyopen.annotation.Api;
import com.gitee.easyopen.annotation.ApiService;
import com.gitee.easyopen.doc.annotation.ApiDoc;
import com.gitee.easyopen.doc.annotation.ApiDocMethod;
import com.gitee.easyopen.util.CopyUtil;
import com.gitee.fastmybatis.core.PageInfo;
import com.gitee.fastmybatis.core.query.Query;
import com.gitee.fastmybatis.core.query.Sort;
import com.gitee.fastmybatis.core.support.PageEasyui;
import com.gitee.fastmybatis.core.util.MapperUtil;
import com.gitee.sop.adminserver.api.isv.param.RoleForm;
import com.gitee.sop.adminserver.api.isv.param.RolePageParam;
import com.gitee.sop.adminserver.api.isv.result.RoleVO;
import com.gitee.sop.adminserver.common.BizException;
import com.gitee.sop.adminserver.entity.PermRole;
import com.gitee.sop.adminserver.entity.PermRolePermission;
import com.gitee.sop.adminserver.mapper.PermRoleMapper;
import com.gitee.sop.adminserver.mapper.PermRolePermissionMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -28,6 +34,9 @@ public class RoleApi {
@Autowired
PermRoleMapper permRoleMapper;
@Autowired
PermRolePermissionMapper permRolePermissionMapper;
@Api(name = "role.listall")
List<RoleVO> roleListall() {
Query query = new Query();
@ -40,4 +49,39 @@ public class RoleApi {
})
.collect(Collectors.toList());
}
@ApiDocMethod(description = "获取角色,分页")
@Api(name = "role.page")
PageEasyui<RoleVO> pageRole(RolePageParam rolePage) {
Query query = Query.build(rolePage);
return MapperUtil.queryForEasyuiDatagrid(permRoleMapper, query, RoleVO.class);
}
@Api(name = "role.add")
void addRole(RoleForm roleForm) {
PermRole rec = permRoleMapper.getByColumn("role_code", roleForm.getRoleCode());
if (rec != null) {
throw new BizException("角色码已存在");
}
PermRole permRole = new PermRole();
CopyUtil.copyPropertiesIgnoreNull(roleForm, permRole);
permRoleMapper.saveIgnoreNull(permRole);
}
@Api(name = "role.update")
void updateRole(RoleForm roleForm) {
PermRole rec = permRoleMapper.getById(roleForm.getId());
rec.setDescription(roleForm.getDescription());
permRoleMapper.updateIgnoreNull(rec);
}
@Api(name = "role.del")
void delRole(long id) {
PermRole rec = permRoleMapper.getById(id);
PermRolePermission rolePermission = permRolePermissionMapper.getByColumn("role_code", rec.getRoleCode());
if (rolePermission != null) {
throw new BizException("该角色已使用,无法删除");
}
permRoleMapper.deleteById(id);
}
}

@ -0,0 +1,24 @@
package com.gitee.sop.adminserver.api.isv.param;
import com.gitee.easyopen.doc.annotation.ApiDocField;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.NotBlank;
/**
* @author tanghc
*/
@Data
public class RoleForm {
@ApiDocField(description = "id")
private Long id;
@ApiDocField(description = "角色码")
@NotBlank(message = "roleCode不能为空")
@Length(max = 64)
private String roleCode;
@ApiDocField(description = "描述")
private String description;
}

@ -0,0 +1,19 @@
package com.gitee.sop.adminserver.api.isv.param;
import com.gitee.easyopen.doc.annotation.ApiDocField;
import com.gitee.fastmybatis.core.query.Operator;
import com.gitee.fastmybatis.core.query.annotation.Condition;
import com.gitee.fastmybatis.core.query.param.PageParam;
import lombok.Getter;
import lombok.Setter;
/**
* @author tanghc
*/
@Getter
@Setter
public class RolePageParam extends PageParam {
@ApiDocField(description = "角色码")
@Condition(operator = Operator.like)
private String roleCode;
}

@ -10,10 +10,18 @@ import java.util.Date;
*/
@Data
public class RoleVO {
@ApiDocField(description = "id")
private Long id;
@ApiDocField(description = "角色码")
private String roleCode;
@ApiDocField(description = "描述")
private String description;
@ApiDocField(description = "创建时间")
private Date gmtCreate;
@ApiDocField(description = "修改时间")
private Date gmtModified;
}

@ -28,8 +28,15 @@ public class IsvInfo {
/** appKey, 数据库字段:app_key */
private String appKey;
// 不再使用,转移到isv_keys表
private String secret = "";
/** 公钥,不再使用,转移到isv_keys表 数据库字段:pub_key */
private String pubKey;
/** 私钥,不再使用,转移到isv_keys表 数据库字段:pri_key */
private String priKey;
/** 1启用,2禁用, 数据库字段:status */
private Byte status;

@ -1 +1 @@
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><link rel=icon href=favicon.ico><title>SOP Admin</title><link href=static/css/chunk-elementUI.81cf475c.css rel=stylesheet><link href=static/css/chunk-libs.3dfb7769.css rel=stylesheet><link href=static/css/app.4f0872ef.css rel=stylesheet></head><body><noscript><strong>We're sorry but SOP Admin doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script>(function(e){function t(t){for(var r,c,a=t[0],i=t[1],f=t[2],l=0,h=[];l<a.length;l++)c=a[l],u[c]&&h.push(u[c][0]),u[c]=0;for(r in i)Object.prototype.hasOwnProperty.call(i,r)&&(e[r]=i[r]);s&&s(t);while(h.length)h.shift()();return o.push.apply(o,f||[]),n()}function n(){for(var e,t=0;t<o.length;t++){for(var n=o[t],r=!0,c=1;c<n.length;c++){var a=n[c];0!==u[a]&&(r=!1)}r&&(o.splice(t--,1),e=i(i.s=n[0]))}return e}var r={},c={runtime:0},u={runtime:0},o=[];function a(e){return i.p+"static/js/"+({}[e]||e)+"."+{"chunk-238a81e9":"5955f13d","chunk-25908fca":"3daff0c2","chunk-29e7142c":"994a3ac0","chunk-2d22c2e3":"46f37153","chunk-37401378":"4e39ec9b","chunk-4a59cbe4":"b5e74cc4","chunk-5f00e420":"5e409513","chunk-6a68a33e":"f59ae895","chunk-73b2dcec":"094bb2fa"}[e]+".js"}function i(t){if(r[t])return r[t].exports;var n=r[t]={i:t,l:!1,exports:{}};return e[t].call(n.exports,n,n.exports,i),n.l=!0,n.exports}i.e=function(e){var t=[],n={"chunk-238a81e9":1,"chunk-25908fca":1,"chunk-29e7142c":1,"chunk-37401378":1,"chunk-5f00e420":1,"chunk-6a68a33e":1,"chunk-73b2dcec":1};c[e]?t.push(c[e]):0!==c[e]&&n[e]&&t.push(c[e]=new Promise(function(t,n){for(var r="static/css/"+({}[e]||e)+"."+{"chunk-238a81e9":"e8e2beee","chunk-25908fca":"89ab33e8","chunk-29e7142c":"d10599db","chunk-2d22c2e3":"31d6cfe0","chunk-37401378":"a43114f3","chunk-4a59cbe4":"31d6cfe0","chunk-5f00e420":"aeebecd4","chunk-6a68a33e":"3b12267b","chunk-73b2dcec":"99cf6327"}[e]+".css",u=i.p+r,o=document.getElementsByTagName("link"),a=0;a<o.length;a++){var f=o[a],l=f.getAttribute("data-href")||f.getAttribute("href");if("stylesheet"===f.rel&&(l===r||l===u))return t()}var h=document.getElementsByTagName("style");for(a=0;a<h.length;a++){f=h[a],l=f.getAttribute("data-href");if(l===r||l===u)return t()}var s=document.createElement("link");s.rel="stylesheet",s.type="text/css",s.onload=t,s.onerror=function(t){var r=t&&t.target&&t.target.src||u,o=new Error("Loading CSS chunk "+e+" failed.\n("+r+")");o.code="CSS_CHUNK_LOAD_FAILED",o.request=r,delete c[e],s.parentNode.removeChild(s),n(o)},s.href=u;var d=document.getElementsByTagName("head")[0];d.appendChild(s)}).then(function(){c[e]=0}));var r=u[e];if(0!==r)if(r)t.push(r[2]);else{var o=new Promise(function(t,n){r=u[e]=[t,n]});t.push(r[2]=o);var f,l=document.createElement("script");l.charset="utf-8",l.timeout=120,i.nc&&l.setAttribute("nonce",i.nc),l.src=a(e),f=function(t){l.onerror=l.onload=null,clearTimeout(h);var n=u[e];if(0!==n){if(n){var r=t&&("load"===t.type?"missing":t.type),c=t&&t.target&&t.target.src,o=new Error("Loading chunk "+e+" failed.\n("+r+": "+c+")");o.type=r,o.request=c,n[1](o)}u[e]=void 0}};var h=setTimeout(function(){f({type:"timeout",target:l})},12e4);l.onerror=l.onload=f,document.head.appendChild(l)}return Promise.all(t)},i.m=e,i.c=r,i.d=function(e,t,n){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},i.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(i.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)i.d(n,r,function(t){return e[t]}.bind(null,r));return n},i.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i.oe=function(e){throw console.error(e),e};var f=window["webpackJsonp"]=window["webpackJsonp"]||[],l=f.push.bind(f);f.push=t,f=f.slice();for(var h=0;h<f.length;h++)t(f[h]);var s=l;n()})([]);</script><script src=static/js/chunk-elementUI.8ebdfbab.js></script><script src=static/js/chunk-libs.9cf9cc40.js></script><script src=static/js/app.3cc1fa6b.js></script></body></html>
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><link rel=icon href=favicon.ico><title>SOP Admin</title><link href=static/css/chunk-elementUI.81cf475c.css rel=stylesheet><link href=static/css/chunk-libs.3dfb7769.css rel=stylesheet><link href=static/css/app.4f0872ef.css rel=stylesheet></head><body><noscript><strong>We're sorry but SOP Admin doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script>(function(e){function t(t){for(var r,c,a=t[0],i=t[1],f=t[2],l=0,d=[];l<a.length;l++)c=a[l],u[c]&&d.push(u[c][0]),u[c]=0;for(r in i)Object.prototype.hasOwnProperty.call(i,r)&&(e[r]=i[r]);h&&h(t);while(d.length)d.shift()();return o.push.apply(o,f||[]),n()}function n(){for(var e,t=0;t<o.length;t++){for(var n=o[t],r=!0,c=1;c<n.length;c++){var a=n[c];0!==u[a]&&(r=!1)}r&&(o.splice(t--,1),e=i(i.s=n[0]))}return e}var r={},c={runtime:0},u={runtime:0},o=[];function a(e){return i.p+"static/js/"+({}[e]||e)+"."+{"chunk-238a81e9":"5955f13d","chunk-25908fca":"e7e4b6d5","chunk-29e7142c":"994a3ac0","chunk-2d2085ef":"a63a74dc","chunk-2d22c2e3":"46f37153","chunk-37401378":"4e39ec9b","chunk-4a59cbe4":"b5e74cc4","chunk-5f00e420":"5e409513","chunk-6a68a33e":"f59ae895","chunk-73b2dcec":"094bb2fa"}[e]+".js"}function i(t){if(r[t])return r[t].exports;var n=r[t]={i:t,l:!1,exports:{}};return e[t].call(n.exports,n,n.exports,i),n.l=!0,n.exports}i.e=function(e){var t=[],n={"chunk-238a81e9":1,"chunk-25908fca":1,"chunk-29e7142c":1,"chunk-37401378":1,"chunk-5f00e420":1,"chunk-6a68a33e":1,"chunk-73b2dcec":1};c[e]?t.push(c[e]):0!==c[e]&&n[e]&&t.push(c[e]=new Promise(function(t,n){for(var r="static/css/"+({}[e]||e)+"."+{"chunk-238a81e9":"e8e2beee","chunk-25908fca":"89ab33e8","chunk-29e7142c":"d10599db","chunk-2d2085ef":"31d6cfe0","chunk-2d22c2e3":"31d6cfe0","chunk-37401378":"a43114f3","chunk-4a59cbe4":"31d6cfe0","chunk-5f00e420":"aeebecd4","chunk-6a68a33e":"3b12267b","chunk-73b2dcec":"99cf6327"}[e]+".css",u=i.p+r,o=document.getElementsByTagName("link"),a=0;a<o.length;a++){var f=o[a],l=f.getAttribute("data-href")||f.getAttribute("href");if("stylesheet"===f.rel&&(l===r||l===u))return t()}var d=document.getElementsByTagName("style");for(a=0;a<d.length;a++){f=d[a],l=f.getAttribute("data-href");if(l===r||l===u)return t()}var h=document.createElement("link");h.rel="stylesheet",h.type="text/css",h.onload=t,h.onerror=function(t){var r=t&&t.target&&t.target.src||u,o=new Error("Loading CSS chunk "+e+" failed.\n("+r+")");o.code="CSS_CHUNK_LOAD_FAILED",o.request=r,delete c[e],h.parentNode.removeChild(h),n(o)},h.href=u;var s=document.getElementsByTagName("head")[0];s.appendChild(h)}).then(function(){c[e]=0}));var r=u[e];if(0!==r)if(r)t.push(r[2]);else{var o=new Promise(function(t,n){r=u[e]=[t,n]});t.push(r[2]=o);var f,l=document.createElement("script");l.charset="utf-8",l.timeout=120,i.nc&&l.setAttribute("nonce",i.nc),l.src=a(e),f=function(t){l.onerror=l.onload=null,clearTimeout(d);var n=u[e];if(0!==n){if(n){var r=t&&("load"===t.type?"missing":t.type),c=t&&t.target&&t.target.src,o=new Error("Loading chunk "+e+" failed.\n("+r+": "+c+")");o.type=r,o.request=c,n[1](o)}u[e]=void 0}};var d=setTimeout(function(){f({type:"timeout",target:l})},12e4);l.onerror=l.onload=f,document.head.appendChild(l)}return Promise.all(t)},i.m=e,i.c=r,i.d=function(e,t,n){i.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},i.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},i.t=function(e,t){if(1&t&&(e=i(e)),8&t)return e;if(4&t&&"object"===typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(i.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var r in e)i.d(n,r,function(t){return e[t]}.bind(null,r));return n},i.n=function(e){var t=e&&e.__esModule?function(){return e["default"]}:function(){return e};return i.d(t,"a",t),t},i.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},i.p="",i.oe=function(e){throw console.error(e),e};var f=window["webpackJsonp"]=window["webpackJsonp"]||[],l=f.push.bind(f);f.push=t,f=f.slice();for(var d=0;d<f.length;d++)t(f[d]);var h=l;n()})([]);</script><script src=static/js/chunk-elementUI.8ebdfbab.js></script><script src=static/js/chunk-libs.9cf9cc40.js></script><script src=static/js/app.61234abc.js></script></body></html>

@ -91,8 +91,8 @@ export const constantRoutes = [
{
path: '/isv',
component: Layout,
name: 'Isv',
meta: { title: 'ISV管理', icon: 'user' },
redirect: '/isv/list',
children: [
{
path: 'list',
@ -100,6 +100,12 @@ export const constantRoutes = [
component: () => import('@/views/isv/index'),
meta: { title: 'ISV列表' }
},
{
path: 'role',
name: 'Role',
component: () => import('@/views/isv/role'),
meta: { title: '角色管理' }
},
{
path: 'keys',
name: 'Keys',

@ -288,7 +288,7 @@ export default {
},
onIsvDialogSave: function() {
const that = this
this.$refs['isvForm'].validate((valid) => {
this.$refs.isvForm.validate((valid) => {
if (valid) {
const uri = this.isvDialogFormData.id === 0 ? 'isv.info.add' : 'isv.info.update'
that.post(uri, that.isvDialogFormData, function() {

@ -0,0 +1,179 @@
<template>
<div class="app-container">
<el-form :inline="true" :model="searchFormData" class="demo-form-inline" size="mini">
<el-form-item label="角色码">
<el-input v-model="searchFormData.roleCode" :clearable="true" placeholder="输入角色码" style="width: 250px;" />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="loadTable">查询</el-button>
</el-form-item>
</el-form>
<el-button type="primary" size="mini" icon="el-icon-plus" style="margin-bottom: 10px;" @click="onAdd">新增角色</el-button>
<el-table
:data="pageInfo.rows"
border
highlight-current-row
>
<el-table-column
prop="roleCode"
label="角色码"
width="200"
/>
<el-table-column
prop="description"
label="角色描述"
width="200"
/>
<el-table-column
prop="gmtCreate"
label="添加时间"
width="160"
/>
<el-table-column
prop="gmtModified"
label="修改时间"
width="160"
/>
<el-table-column
label="操作"
width="150"
>
<template slot-scope="scope">
<el-button type="text" size="mini" @click="onTableUpdate(scope.row)">修改</el-button>
<el-button type="text" size="mini" @click="onTableDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<el-pagination
background
style="margin-top: 5px"
:current-page="searchFormData.pageIndex"
:page-size="searchFormData.pageSize"
:page-sizes="[5, 10, 20, 40]"
:total="pageInfo.total"
layout="total, sizes, prev, pager, next"
@size-change="onSizeChange"
@current-change="onPageIndexChange"
/>
<!--dialog-->
<el-dialog
:title="roleDialogTitle"
:visible.sync="roleDialogVisible"
:close-on-click-modal="false"
@close="resetForm('roleForm')"
>
<el-form
ref="roleForm"
:rules="roleDialogFormRules"
:model="roleDialogFormData"
label-width="120px"
size="mini"
>
<el-form-item prop="roleCode" label="角色码">
<el-input v-show="roleDialogFormData.id === 0" v-model="roleDialogFormData.roleCode" />
<span v-show="roleDialogFormData.id > 0">{{ roleDialogFormData.roleCode }}</span>
</el-form-item>
<el-form-item prop="description" label="角色描述">
<el-input v-model="roleDialogFormData.description" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="roleDialogVisible = false"> </el-button>
<el-button type="primary" @click="onRoleDialogSave"> </el-button>
</div>
</el-dialog>
</div>
</template>
<script>
export default {
data() {
return {
searchFormData: {
roleCode: '',
pageIndex: 1,
pageSize: 10
},
pageInfo: {
rows: [],
total: 0
},
roleDialogVisible: false,
roleDialogTitle: '',
roleDialogFormData: {
id: 0,
roleCode: '',
description: ''
},
roleDialogFormRules: {
roleCode: [
{ required: true, message: '不能为空', trigger: 'blur' },
{ min: 1, max: 64, message: '长度在 1 到 100 个字符', trigger: 'blur' }
],
description: [
{ max: 64, message: '不能超过 64 个字符', trigger: 'blur' }
]
},
rulesIsvForm: {
appKey: [
{ required: true, message: '不能为空', trigger: 'blur' },
{ min: 1, max: 100, message: '长度在 1 到 100 个字符', trigger: 'blur' }
]
}
}
},
created() {
this.loadTable()
},
methods: {
loadTable: function() {
this.post('role.page', this.searchFormData, function(resp) {
this.pageInfo = resp.data
})
},
onTableUpdate: function(row) {
this.roleDialogTitle = '修改角色'
this.roleDialogVisible = true
this.$nextTick(() => {
Object.assign(this.roleDialogFormData, row)
})
},
onTableDelete: function(row) {
this.confirm(`确认要删除角色【${row.roleCode}】吗?`, function(done) {
const data = {
id: row.id
}
this.post('role.del', data, function() {
done()
this.tip('删除成功')
this.loadTable()
})
})
},
onRoleDialogSave: function() {
this.$refs.roleForm.validate((valid) => {
if (valid) {
const uri = this.roleDialogFormData.id ? 'role.update' : 'role.add'
this.post(uri, this.roleDialogFormData, function() {
this.roleDialogVisible = false
this.loadTable()
})
}
})
},
onSizeChange: function(size) {
this.searchFormData.pageSize = size
this.loadTable()
},
onAdd: function() {
this.roleDialogTitle = '新增角色'
this.roleDialogVisible = true
this.roleDialogFormData.id = 0
},
onPageIndexChange: function(pageIndex) {
this.searchFormData.pageIndex = pageIndex
this.loadTable()
}
}
}
</script>

@ -78,7 +78,7 @@ CREATE TABLE `config_route_limit` (
CREATE TABLE `isv_info` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`app_key` varchar(100) NOT NULL COMMENT 'appKey',
`secret` varchar(200) NOT NULL COMMENT 'secret',
`secret` varchar(200) COMMENT 'secret',
`pub_key` text COMMENT '公钥',
`pri_key` text COMMENT '私钥',
`status` tinyint(4) unsigned NOT NULL DEFAULT '0' COMMENT '1启用,2禁用',

Loading…
Cancel
Save