Merge branch 'develop'

master
eason 7 years ago
commit e5dc84fa50

@ -0,0 +1,20 @@
package au.com.royalpay.payment.manage.logview.core;
import au.com.royalpay.payment.manage.merchants.beans.mongo.ClientConfigLog;
import com.alibaba.fastjson.JSONObject;
import com.github.miemiedev.mybatis.paginator.domain.PageBounds;
import java.util.List;
/**
* @author kira
* @date 2018/6/15
*/
public interface OperationLogService {
List<ClientConfigLog> query(JSONObject params);
JSONObject query(JSONObject params, PageBounds pageBounds);
}

@ -0,0 +1,66 @@
package au.com.royalpay.payment.manage.logview.core.impl;
import au.com.royalpay.payment.manage.logview.core.OperationLogService;
import au.com.royalpay.payment.manage.merchants.beans.mongo.ClientConfigLog;
import au.com.royalpay.payment.manage.merchants.core.ClientManager;
import com.alibaba.fastjson.JSONObject;
import com.github.miemiedev.mybatis.paginator.domain.PageBounds;
import com.github.miemiedev.mybatis.paginator.domain.Paginator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
import java.util.List;
import javax.annotation.Resource;
/**
* @author kira
* @date 2018/6/15
*/
@Service
public class OperationLogServiceImpl implements OperationLogService {
@Resource
private MongoTemplate mongoTemplate;
@Resource
private ClientManager clientManager;
@Override
public JSONObject query(JSONObject params, PageBounds pageBounds) {
if (StringUtils.isNotEmpty(params.getString("client_moniker"))) {
JSONObject client = clientManager.getClientInfoByMoniker(params.getString("client_moniker"));
if(client!=null) {
params.put("client_id", client.getIntValue("client_id"));
}
}
Query query = new Query();
if (params.getIntValue("client_id") != 0) {
query.addCriteria(Criteria.where("clientId").is(params.getIntValue("client_id")));
}
List<ClientConfigLog> clientConfigLogList = mongoTemplate.find(query, ClientConfigLog.class);
return buildPageListResult(clientConfigLogList,
new Paginator(pageBounds.getPage(), pageBounds.getLimit(), (int) mongoTemplate.count(query, ClientConfigLog.class)));
}
@Override
public List<ClientConfigLog> query(JSONObject params) {
Query query = new Query();
query.addCriteria(Criteria.where("clientId").is(params.getIntValue("client_id")));
List<ClientConfigLog> clientConfigLogList = mongoTemplate.find(query, ClientConfigLog.class);
Paginator paginator = new Paginator(1, 1, 1);
return null;
}
public static JSONObject buildPageListResult(List datas, Paginator paginator) {
JSONObject res = new JSONObject();
res.put("data", datas);
res.put("pagination", paginator);
return res;
}
}

@ -0,0 +1,30 @@
package au.com.royalpay.payment.manage.logview.web;
import au.com.royalpay.payment.manage.logview.core.OperationLogService;
import com.alibaba.fastjson.JSONObject;
import com.github.miemiedev.mybatis.paginator.domain.PageBounds;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* Created by Tayl0r on 2017/5/3.
*/
@RestController
@RequestMapping(value = "/sys_logs/config/operation")
public class ConfigOperationController {
@Resource
private OperationLogService operationLogService;
@RequestMapping(method = RequestMethod.GET)
public JSONObject list(@RequestParam(required = false) String client_moniker,@RequestParam(defaultValue = "1") int page,@RequestParam(defaultValue = "10") int limit){
JSONObject params = new JSONObject();
params.put("client_moniker",client_moniker);
return operationLogService.query(params,new PageBounds(page,limit));
}
}

@ -27,6 +27,9 @@ public interface MailUnsubMapper {
@AutoSql(type = SqlType.SELECT) @AutoSql(type = SqlType.SELECT)
JSONObject getOne(@Param("id") Long id,@Param("address") String address); JSONObject getOne(@Param("id") Long id,@Param("address") String address);
@AutoSql(type = SqlType.SELECT)
JSONObject findOneByClientMoniker(@Param("client_moniker") String client_moniker);
PageList<JSONObject> queryPageable(JSONObject params, PageBounds pagination); PageList<JSONObject> queryPageable(JSONObject params, PageBounds pagination);
List<JSONObject> query(JSONObject params); List<JSONObject> query(JSONObject params);

@ -60,6 +60,9 @@ public interface ClientManager {
@Transactional @Transactional
void updateClientPaymentConfig(JSONObject manager, String clientMoniker, JSONObject subMerchantInfo); void updateClientPaymentConfig(JSONObject manager, String clientMoniker, JSONObject subMerchantInfo);
@Transactional
void updateAliSubMerchantId(JSONObject manager, String clientMoniker, JSONObject aliSubMerchantInfo);
@Transactional(noRollbackFor = EmailException.class) @Transactional(noRollbackFor = EmailException.class)
void auditClient(JSONObject manager, String clientMoniker, int pass); void auditClient(JSONObject manager, String clientMoniker, int pass);
@ -258,6 +261,8 @@ public interface ClientManager {
void enableGatewayUpgrade(JSONObject account,String clientMoniker, boolean gatewayUpgrade); void enableGatewayUpgrade(JSONObject account,String clientMoniker, boolean gatewayUpgrade);
void enableGatewayAlipayOnline(JSONObject account,String clientMoniker, boolean gatewayAlipayOnline);
void setCustomerSurchargeRate(JSONObject account,String clientMoniker, BigDecimal customer_surcharge_rate); void setCustomerSurchargeRate(JSONObject account,String clientMoniker, BigDecimal customer_surcharge_rate);
void setOrderExpiryConfig(JSONObject account,String clientMoniker, String orderExpiryConfig); void setOrderExpiryConfig(JSONObject account,String clientMoniker, String orderExpiryConfig);
@ -310,4 +315,8 @@ public interface ClientManager {
JSONObject getByEmail(String email, int page, int limit,List<String> ExceptClientIds); JSONObject getByEmail(String email, int page, int limit,List<String> ExceptClientIds);
void addSub(String client_moniker, JSONObject manager);
void removeSub(String client_moniker, JSONObject manager);
} }

@ -19,21 +19,7 @@ import au.com.royalpay.payment.manage.mappers.log.LogClientSubMerchantIdMapper;
import au.com.royalpay.payment.manage.mappers.log.LogSettleMailMapper; import au.com.royalpay.payment.manage.mappers.log.LogSettleMailMapper;
import au.com.royalpay.payment.manage.mappers.payment.TransactionMapper; import au.com.royalpay.payment.manage.mappers.payment.TransactionMapper;
import au.com.royalpay.payment.manage.mappers.redpack.ActClientInvitationCodeMapper; import au.com.royalpay.payment.manage.mappers.redpack.ActClientInvitationCodeMapper;
import au.com.royalpay.payment.manage.mappers.system.ClientAccountMapper; import au.com.royalpay.payment.manage.mappers.system.*;
import au.com.royalpay.payment.manage.mappers.system.ClientApplyMapper;
import au.com.royalpay.payment.manage.mappers.system.ClientAuditProcessMapper;
import au.com.royalpay.payment.manage.mappers.system.ClientBDMapper;
import au.com.royalpay.payment.manage.mappers.system.ClientBankAccountMapper;
import au.com.royalpay.payment.manage.mappers.system.ClientDeviceMapper;
import au.com.royalpay.payment.manage.mappers.system.ClientFilesMapper;
import au.com.royalpay.payment.manage.mappers.system.ClientMapper;
import au.com.royalpay.payment.manage.mappers.system.ClientRateMapper;
import au.com.royalpay.payment.manage.mappers.system.ClientsContractMapper;
import au.com.royalpay.payment.manage.mappers.system.CommoditiesMapper;
import au.com.royalpay.payment.manage.mappers.system.MailSendMapper;
import au.com.royalpay.payment.manage.mappers.system.ManagerMapper;
import au.com.royalpay.payment.manage.mappers.system.OrgMapper;
import au.com.royalpay.payment.manage.mappers.system.SysWxMerchantApplyMapper;
import au.com.royalpay.payment.manage.merchants.beans.ActivityPosterBuilder; import au.com.royalpay.payment.manage.merchants.beans.ActivityPosterBuilder;
import au.com.royalpay.payment.manage.merchants.beans.BankAccountInfo; import au.com.royalpay.payment.manage.merchants.beans.BankAccountInfo;
import au.com.royalpay.payment.manage.merchants.beans.ClientAuthFilesInfo; import au.com.royalpay.payment.manage.merchants.beans.ClientAuthFilesInfo;
@ -187,6 +173,8 @@ public class ClientManagerImpl implements ClientManager, ManagerTodoNoticeProvid
@Resource @Resource
private ClientRateMapper clientRateMapper; private ClientRateMapper clientRateMapper;
@Resource @Resource
private MailUnsubMapper mailUnsubMapper;
@Resource
private AttachmentClient attachmentClient; private AttachmentClient attachmentClient;
@Resource @Resource
private StringRedisTemplate stringRedisTemplate; private StringRedisTemplate stringRedisTemplate;
@ -326,6 +314,7 @@ public class ClientManagerImpl implements ClientManager, ManagerTodoNoticeProvid
checkClientOrg(manager, client); checkClientOrg(manager, client);
} }
client.putAll(clientConfigService.find(client.getIntValue("client_id"))); client.putAll(clientConfigService.find(client.getIntValue("client_id")));
client.put("unsubscribe",mailUnsubMapper.findOneByClientMoniker(clientMoniker) == null?false:true);
client.put("show_all_permission", true); client.put("show_all_permission", true);
int role = manager != null ? manager.getIntValue("role") : 0; int role = manager != null ? manager.getIntValue("role") : 0;
if (manager != null) { if (manager != null) {
@ -722,6 +711,21 @@ public class ClientManagerImpl implements ClientManager, ManagerTodoNoticeProvid
clientInfoCacheSupport.clearClientCache(clientId); clientInfoCacheSupport.clearClientCache(clientId);
} }
@Override
public void updateAliSubMerchantId(JSONObject manager, String clientMoniker, JSONObject aliSubMerchantInfo) {
JSONObject client = getClientInfoByMoniker(clientMoniker);
if (client == null) {
throw new InvalidShortIdException();
}
checkOrgPermission(manager, client);
JSONObject update = new JSONObject();
int clientId = client.getIntValue("client_id");
update.put("client_id", clientId);
update.put("ali_sub_merchant_id", aliSubMerchantInfo.getString("ali_sub_merchant_id"));
clientMapper.update(update);
clientInfoCacheSupport.clearClientCache(clientId);
}
private void recordSubMerchantLog(JSONObject client, JSONObject subMerchantInfo, JSONObject manager) { private void recordSubMerchantLog(JSONObject client, JSONObject subMerchantInfo, JSONObject manager) {
JSONObject log = new JSONObject(); JSONObject log = new JSONObject();
log.put("sub_merchant_id_after", subMerchantInfo.getString("sub_merchant_id")); log.put("sub_merchant_id_after", subMerchantInfo.getString("sub_merchant_id"));
@ -3061,6 +3065,15 @@ public class ClientManagerImpl implements ClientManager, ManagerTodoNoticeProvid
clientModifySupport.processClientConfigModify(new SwitchPermissionModify(account, clientMoniker, "gateway_upgrade", gatewayUpgrade)); clientModifySupport.processClientConfigModify(new SwitchPermissionModify(account, clientMoniker, "gateway_upgrade", gatewayUpgrade));
} }
@Override
public void enableGatewayAlipayOnline(JSONObject account, String clientMoniker, boolean gatewayAlipayOnline) {
JSONObject client = getClientInfoByMoniker(clientMoniker);
if (client == null) {
throw new InvalidShortIdException();
}
clientModifySupport.processClientConfigModify(new SwitchPermissionModify(account, clientMoniker, "gateway_alipay_online", gatewayAlipayOnline));
}
private void sendMessagetoCompliance(JSONObject client, String bd_user_name) { private void sendMessagetoCompliance(JSONObject client, String bd_user_name) {
List<JSONObject> complianceList = managerMapper.getOnlyCompliance(); List<JSONObject> complianceList = managerMapper.getOnlyCompliance();
if (complianceList != null && complianceList.size() > 0) { if (complianceList != null && complianceList.size() > 0) {
@ -3623,4 +3636,28 @@ public class ClientManagerImpl implements ClientManager, ManagerTodoNoticeProvid
param.put("except_client_ids",exceptClientIds); param.put("except_client_ids",exceptClientIds);
return PageListUtils.buildPageListResult(clientMapper.simpleQuery(param,new PageBounds(page, limit))); return PageListUtils.buildPageListResult(clientMapper.simpleQuery(param,new PageBounds(page, limit)));
} }
@Override
public void addSub(String client_moniker, JSONObject manager) {
JSONObject client = getClientInfoByMoniker(client_moniker);
if (client == null) {
throw new InvalidShortIdException();
}
checkOrgPermission(manager, client);
mailService.addUnsub(client_moniker);
}
@Override
public void removeSub(String client_moniker, JSONObject manager) {
JSONObject client = getClientInfoByMoniker(client_moniker);
if (client == null) {
throw new InvalidShortIdException();
}
checkOrgPermission(manager, client);
JSONObject sub = mailUnsubMapper.findOneByClientMoniker(client_moniker);
if(sub == null){
throw new BadRequestException();
}
mailService.removeUnsub(sub.getLong("id"));
}
} }

@ -154,6 +154,10 @@ public class PartnerManageController {
public void updatePartnerPaymentConfig(@PathVariable String clientMoniker, @RequestBody JSONObject subMerchantInfo, @ModelAttribute(CommonConsts.MANAGER_STATUS) JSONObject manager) { public void updatePartnerPaymentConfig(@PathVariable String clientMoniker, @RequestBody JSONObject subMerchantInfo, @ModelAttribute(CommonConsts.MANAGER_STATUS) JSONObject manager) {
clientManager.updateClientPaymentConfig(manager, clientMoniker, subMerchantInfo); clientManager.updateClientPaymentConfig(manager, clientMoniker, subMerchantInfo);
} }
@ManagerMapping(value = "/{clientMoniker}/ali_sub_merchant_id", method = RequestMethod.PUT, role = {ManagerRole.OPERATOR})
public void updateAliSubMerchantId(@PathVariable String clientMoniker, @RequestBody JSONObject aliSubMerchantInfo, @ModelAttribute(CommonConsts.MANAGER_STATUS) JSONObject manager) {
clientManager.updateAliSubMerchantId(manager, clientMoniker, aliSubMerchantInfo);
}
@ManagerMapping(value = "/{clientMoniker}/qrcode_surcharge", method = RequestMethod.PUT, role = {ManagerRole.OPERATOR, ManagerRole.ADMIN, ManagerRole.BD_USER, ManagerRole.SERVANT}) @ManagerMapping(value = "/{clientMoniker}/qrcode_surcharge", method = RequestMethod.PUT, role = {ManagerRole.OPERATOR, ManagerRole.ADMIN, ManagerRole.BD_USER, ManagerRole.SERVANT})
public void setClientPaySurCharge(@ModelAttribute(CommonConsts.MANAGER_STATUS) JSONObject manager,@PathVariable String clientMoniker, @RequestBody JSONObject config) { public void setClientPaySurCharge(@ModelAttribute(CommonConsts.MANAGER_STATUS) JSONObject manager,@PathVariable String clientMoniker, @RequestBody JSONObject config) {
@ -165,6 +169,11 @@ public class PartnerManageController {
clientManager.enableGatewayUpgrade(manager,clientMoniker, config.getBooleanValue("gateway_upgrade")); clientManager.enableGatewayUpgrade(manager,clientMoniker, config.getBooleanValue("gateway_upgrade"));
} }
@ManagerMapping(value = "/{clientMoniker}/gateway_alipay_online", method = RequestMethod.PUT, role = {ManagerRole.DEVELOPER})
public void enableGatewayAlipayOnline(@ModelAttribute(CommonConsts.MANAGER_STATUS) JSONObject manager,@PathVariable String clientMoniker, @RequestBody JSONObject config) {
clientManager.enableGatewayAlipayOnline(manager,clientMoniker, config.getBooleanValue("gateway_alipay_online"));
}
@ManagerMapping(value = "/{clientMoniker}/api_surcharge", method = RequestMethod.PUT, role = {ManagerRole.OPERATOR, ManagerRole.ADMIN, ManagerRole.BD_USER, ManagerRole.SERVANT}) @ManagerMapping(value = "/{clientMoniker}/api_surcharge", method = RequestMethod.PUT, role = {ManagerRole.OPERATOR, ManagerRole.ADMIN, ManagerRole.BD_USER, ManagerRole.SERVANT})
public void setClientApiPaySurCharge(@ModelAttribute(CommonConsts.MANAGER_STATUS) JSONObject manager,@PathVariable String clientMoniker, @RequestBody JSONObject config) { public void setClientApiPaySurCharge(@ModelAttribute(CommonConsts.MANAGER_STATUS) JSONObject manager,@PathVariable String clientMoniker, @RequestBody JSONObject config) {
clientManager.setClientApiPaySurCharge(manager,clientMoniker, config.getBooleanValue("api_surcharge")); clientManager.setClientApiPaySurCharge(manager,clientMoniker, config.getBooleanValue("api_surcharge"));
@ -517,6 +526,14 @@ public class PartnerManageController {
public List<JSONObject> getClientSubMerchantIdLogs(@PathVariable String clientMoniker, @ModelAttribute(CommonConsts.MANAGER_STATUS) JSONObject manager) { public List<JSONObject> getClientSubMerchantIdLogs(@PathVariable String clientMoniker, @ModelAttribute(CommonConsts.MANAGER_STATUS) JSONObject manager) {
return clientManager.getClientSubMerchantIdLogs(clientMoniker,manager); return clientManager.getClientSubMerchantIdLogs(clientMoniker,manager);
} }
@ManagerMapping(value = "/unsub/{clientMoniker}",method = RequestMethod.PUT,role = {ManagerRole.OPERATOR})
public void addSub(@PathVariable String clientMoniker, @ModelAttribute(CommonConsts.MANAGER_STATUS) JSONObject manager) {
clientManager.addSub(clientMoniker,manager);
}
@ManagerMapping(value = "/unsub/{clientMoniker}",method = RequestMethod.DELETE,role = {ManagerRole.OPERATOR})
public void removeSub(@PathVariable String clientMoniker, @ModelAttribute(CommonConsts.MANAGER_STATUS) JSONObject manager) {
clientManager.removeSub(clientMoniker,manager);
}
} }

@ -13,6 +13,7 @@ import au.com.royalpay.payment.tools.utils.id.IdUtil;
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.github.miemiedev.mybatis.paginator.domain.Order;
import com.github.miemiedev.mybatis.paginator.domain.PageBounds; import com.github.miemiedev.mybatis.paginator.domain.PageBounds;
import org.apache.commons.codec.digest.DigestUtils; import org.apache.commons.codec.digest.DigestUtils;
@ -200,7 +201,7 @@ public class MailServiceImp implements MailService {
@Override @Override
public JSONObject queryUnsubPageable(JSONObject params, int limit, int page) { public JSONObject queryUnsubPageable(JSONObject params, int limit, int page) {
return PageListUtils.buildPageListResult(mailUnsubMapper.queryPageable(params,new PageBounds(page, limit))); return PageListUtils.buildPageListResult(mailUnsubMapper.queryPageable(params,new PageBounds(page, limit, Order.formString("create_time.desc"))));
} }
@ -212,7 +213,7 @@ public class MailServiceImp implements MailService {
} }
JSONObject existRecord = mailUnsubMapper.getOne(null,client.getString("contact_email")); JSONObject existRecord = mailUnsubMapper.getOne(null,client.getString("contact_email"));
if(existRecord!=null){ if(existRecord!=null){
throw new BadRequestException("address has been added"); throw new BadRequestException("Client has already existed");
} }
JSONObject record= new JSONObject(); JSONObject record= new JSONObject();
record.put("id", IdUtil.getId()); record.put("id", IdUtil.getId());

@ -74,7 +74,7 @@ public class ABAFile {
lineBuilder.replace(62, 80, StringUtils.rightPad(DateFormatUtils.format(settleDate, "yyyyMMdd"), 18)); lineBuilder.replace(62, 80, StringUtils.rightPad(DateFormatUtils.format(settleDate, "yyyyMMdd"), 18));
lineBuilder.replace(80, 87, bsbNo(selfBSB)); lineBuilder.replace(80, 87, bsbNo(selfBSB));
lineBuilder.replace(87, 96, StringUtils.leftPad(selfAccountNo, 9)); lineBuilder.replace(87, 96, StringUtils.leftPad(selfAccountNo, 9));
lineBuilder.replace(96, 112, StringUtils.rightPad(company, 16)); lineBuilder.replace(96, 112, StringUtils.left(StringUtils.rightPad(company, 16), 16));
lineBuilder.replace(112, 120, StringUtils.leftPad("0", 8, "0")); lineBuilder.replace(112, 120, StringUtils.leftPad("0", 8, "0"));
return lineBuilder.toString(); return lineBuilder.toString();
} }

@ -2,13 +2,12 @@ package au.com.royalpay.payment.manage.system.web;
import au.com.royalpay.payment.manage.notice.core.MailService; import au.com.royalpay.payment.manage.notice.core.MailService;
import au.com.royalpay.payment.manage.permission.manager.RequireManager;
import au.com.royalpay.payment.tools.permission.enums.ManagerRole;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import org.springframework.web.bind.annotation.PathVariable; import org.apache.commons.lang3.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource; import javax.annotation.Resource;
@ -29,9 +28,9 @@ public class MailController {
mailService.removeUnsub(id); mailService.removeUnsub(id);
} }
@RequestMapping(value = "/unsub",method = RequestMethod.POST) @RequestMapping(value = "/unsub/{client_moniker}",method = RequestMethod.PUT)
// @RequireManager(role = {ManagerRole.ADMIN, ManagerRole.BD_USER, ManagerRole.OPERATOR, ManagerRole.SERVANT}) // @RequireManager(role = {ManagerRole.ADMIN, ManagerRole.BD_USER, ManagerRole.OPERATOR, ManagerRole.SERVANT})
public void removeSub(@RequestParam String client_moniker) { public void removeSub(@PathVariable String client_moniker) {
mailService.addUnsub(client_moniker); mailService.addUnsub(client_moniker);
} }
@ -39,8 +38,12 @@ public class MailController {
// @RequireManager(role = {ManagerRole.ADMIN, ManagerRole.BD_USER, ManagerRole.OPERATOR, ManagerRole.SERVANT}) // @RequireManager(role = {ManagerRole.ADMIN, ManagerRole.BD_USER, ManagerRole.OPERATOR, ManagerRole.SERVANT})
public JSONObject list(@RequestParam(required = false) String client_moniker,@RequestParam(required = false) String address,@RequestParam(required = false,defaultValue = "10") int limit ,@RequestParam(required = false,defaultValue = "1") int page) { public JSONObject list(@RequestParam(required = false) String client_moniker,@RequestParam(required = false) String address,@RequestParam(required = false,defaultValue = "10") int limit ,@RequestParam(required = false,defaultValue = "1") int page) {
JSONObject params = new JSONObject(); JSONObject params = new JSONObject();
if(StringUtils.isNotEmpty(client_moniker)){
params.put("client_moniker",client_moniker); params.put("client_moniker",client_moniker);
}
if(StringUtils.isNotEmpty(address)){
params.put("address",address); params.put("address",address);
}
return mailService.queryUnsubPageable(params,limit,page); return mailService.queryUnsubPageable(params,limit,page);
} }
} }

@ -136,20 +136,34 @@
</select> </select>
<select id="countApproved" resultType="com.alibaba.fastjson.JSONObject"> <select id="countApproved" resultType="com.alibaba.fastjson.JSONObject">
<![CDATA[ <![CDATA[
SELECT
b.bd_id, select
b.bd_name, a.bd_id,
sum(b.proportion) num a.bd_name,
FROM sys_client_bd b sum(a.proportion) num
INNER JOIN sys_clients c from sys_client_bd a inner join
ON c.client_id = b.client_id AND b.start_date <= c.approve_time AND (b.end_date > c.approve_time OR b.end_date IS NULL) (select bd_id,
WHERE c.approve_time >= #{begin} AND c.approve_time < #{end} AND c.is_valid = 1 AND c.approve_result = 1 max(create_time) create_time
from sys_client_bd
where client_id in(select client_id from sys_clients c where c.approve_time >= #{begin} AND c.approve_time < #{end} AND c.is_valid = 1 AND c.approve_result = 1
]]> ]]>
<if test="org_id!=null and org_ids==null">and c.org_id=#{org_id}</if> <if test="org_id!=null and org_ids==null">and c.org_id=#{org_id}</if>
<if test="org_ids!=null">and c.org_id in <if test="org_ids!=null">and c.org_id in
<foreach collection="org_ids" item="org_id" open="(" close=")" separator=",">#{org_id}</foreach></if> <foreach collection="org_ids" item="org_id" open="(" close=")" separator=",">#{org_id}</foreach></if>
<if test="source==1">and c.source=1</if> <if test="source==1">and c.source=1</if>
<if test="source==2">and c.source!=1</if> <if test="source==2">and c.source!=1</if>
<![CDATA[
)
and start_date <=#{end}
and is_valid=1
AND (end_date > #{begin} OR end_date IS NULL)
GROUP BY bd_id,client_id
) b
on a.bd_id = b.bd_id and a.create_time = b.create_time
where a.is_valid = 1
]]>
group by bd_id group by bd_id
order by num desc order by num desc
</select> </select>

@ -29,6 +29,10 @@ define(['angular', 'uiRouter'], function (angular) {
url: '/wechatMsg', url: '/wechatMsg',
templateUrl: '/static/config/logview/templates/wechat_msg_log.html', templateUrl: '/static/config/logview/templates/wechat_msg_log.html',
controller: 'wechatMsgLogCtrl' controller: 'wechatMsgLogCtrl'
}).state('logview.config_operation', {
url: '/config_operation',
templateUrl: '/static/config/logview/templates/config_opertaion_log.html',
controller: 'configOperationLogCtrl'
}) })
}]); }]);
app.controller('logviewRootCtrl', ['$scope', function ($scope) { app.controller('logviewRootCtrl', ['$scope', function ($scope) {
@ -110,5 +114,21 @@ define(['angular', 'uiRouter'], function (angular) {
}; };
$scope.listWechatMsgs(); $scope.listWechatMsgs();
}]); }]);
app.controller('configOperationLogCtrl', ['$scope', '$http', '$filter', function ($scope, $http, $filter) {
$scope.pagination = {};
$scope.params = {};
$scope.listConfiglogs = function (page) {
var params = angular.copy($scope.params) || {};
params.page = page || $scope.pagination.page || 1;
params.date = $filter('date')(params.date, 'yyyyMMdd');
$http.get('/sys_logs/config/operation', {params: params}).then(function (resp) {
$scope.logs = resp.data.data;
$scope.pagination = resp.data.pagination;
});
};
$scope.listConfiglogs();
}]);
return app; return app;
}); });

@ -0,0 +1,55 @@
<div class="panel panel-default">
<div class="panel-heading">
<div class="form-inline">
<div class="form-group">
<label class="control-label" for="moniker-input">Client Moniker</label>
<input class="form-control" size="5" id="moniker-input" ng-model="params.client_moniker" ng-enter="listConfiglogs(1)">
</div>
<button class="btn btn-success" type="button" ng-click="listClientLoginLogs(1)">
<i class="fa fa-search"></i> Search
</button>
</div>
</div>
<div class="table-responsive">
<table class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>business</th>
<th>createTime</th>
<th>newData</th>
<th>originData</th>
<th>userId</th>
<th>userName</th>
<th>userType</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="log in logs">
<td ng-bind="log.business"></td>
<td ng-bind="log.createTime"></td>
<td ng-bind="log.newData"></td>
<td ng-bind="log.originData"></td>
<td ng-bind="log.userId"></td>
<td ng-bind="log.userName"></td>
<td ng-bind="log.userType"></td>
</tr>
</tbody>
</table>
</div>
<div class="panel-footer" ng-if="pagination.totalPages>1">
<uib-pagination class="pagination"
total-items="pagination.totalCount"
boundary-links="true"
ng-model="pagination.page"
items-per-page="pagination.limit"
max-size="10"
ng-change="listConfiglogs()"
previous-text="&lsaquo;"
next-text="&rsaquo;"
first-text="&laquo;"
last-text="&raquo;"></uib-pagination>
<div class="row">
<div class="col-xs-12">Total Records:{{pagination.totalCount}};Total Pages:{{pagination.totalPages}}</div>
</div>
</div>
</div>

@ -23,6 +23,10 @@
<i class="fa fa-weixin"></i> <i class="fa fa-weixin"></i>
WeChat Messages WeChat Messages
</a> </a>
<a class="btn btn-app" role="button" ui-sref=".config_operation">
<i class="fa fa-weixin"></i>
Config Operation
</a>
</div> </div>
</div> </div>
</section> </section>

@ -30,6 +30,10 @@ define(['angular', 'uiRouter'], function (angular) {
return $http.get('/sys/permission/modules') return $http.get('/sys/permission/modules')
}] }]
} }
}).state('sysconfig.mail_subscribe', {
url: '/mail',
templateUrl: '/static/config/sysconfigs/templates/mail_subscribe.html',
controller: 'mailSubscribeCtrl'
})/*.state('sysconfig.payment_config',{ })/*.state('sysconfig.payment_config',{
url: '/payment_config', url: '/payment_config',
templateUrl: '/static/config/sysconfigs/templates/payemnt_config.html', templateUrl: '/static/config/sysconfigs/templates/payemnt_config.html',
@ -227,6 +231,55 @@ define(['angular', 'uiRouter'], function (angular) {
}; };
}]); }]);
app.controller('mailSubscribeCtrl', ['$scope', '$http','commonDialog','$uibModal',function ($scope, $http,commonDialog,$uibModal) {
$scope.params = {};
$scope.pagination = {};
$scope.loadUnSubs = function (page) {
var params = angular.copy($scope.params);
params.page = page || $scope.pagination.page || 1;
$http.get('/sys/mail/unsub/query',{params:params}).then(function (resp) {
$scope.mailSubscribes = resp.data.data;
$scope.pagination = resp.data.pagination;
})
};
$scope.deleteSub = function (id) {
commonDialog.confirm({
title: 'Confirm',
content: 'Are you sure?'
}).then(function () {
$http.delete('/sys/mail/unsub/'+id).then(function () {
$scope.loadUnSubs();
}, function (resp) {
commonDialog.alert({title: 'Error!', content: resp.data.message, type: 'error'})
})
})
};
$scope.addUnSub = function () {
$uibModal.open({
templateUrl: '/static/config/sysconfigs/templates/add_mail_unsub.html',
controller: 'addUnSubDialogCtrl',
size: 'sm'
}).result.then(function () {
$scope.loadUnSubs();
});
};
$scope.loadUnSubs();
}]);
app.controller('addUnSubDialogCtrl', ['$scope', '$http', 'commonDialog','$state', function ($scope, $http, commonDialog,$state) {
$scope.unSub = {};
$scope.save = function () {
var unSub = angular.copy($scope.params);
if(!unSub.client_moniker){
alert("client_moniker 不可为空!")
}
$http.put('/sys/mail/unsub/'+unSub.client_moniker).then(function (resp) {
commonDialog.alert({title: 'Success', content: '新增成功', type: 'success'});
$scope.$close();
}, function (resp) {
commonDialog.alert({title: 'Error!', content: resp.data.message, type: 'error'})
})
}
}]);
/*app.controller('paymentConfigCtrl', ['$scope', '$http', 'commonDialog', function ($scope, $http, commonDialog) { /*app.controller('paymentConfigCtrl', ['$scope', '$http', 'commonDialog', function ($scope, $http, commonDialog) {
$scope.loadSysConfigs = function () { $scope.loadSysConfigs = function () {
$http.get('/sysconfig/base').then(function (resp) { $http.get('/sysconfig/base').then(function (resp) {

@ -0,0 +1,13 @@
<div class="content" style="min-height: 100px">
<div class="form-inline">
<div class="form-group">
<input class="form-control" placeholder="Client Moniker" ng-model="params.client_moniker">
</div>
<div class="form-group">
<button class="btn btn-success pull-right" ng-click="save()">Add</button>
</div>
</div>
</div>

@ -0,0 +1,82 @@
<section class="content-header">
<h1>Mail Not Subscribe</h1>
<ol class="breadcrumb">
<li>
<a ui-sref="^"><i class="fa fa-cog"></i> System Config</a>
</li>
<li>Mail Not Subscribe</li>
</ol>
</section>
<div class="content">
<div class="row">
<div class="col-sm-12">
<div class="box-solid">
<div class="box box-warning">
<div class="box-header">
<div class="form-inline">
<div class="form-group">
<input class="form-control" placeholder="Client Moniker" ng-model="params.client_moniker">
</div>
<div class="form-group">
<input class="form-control" placeholder="Mail Address" ng-model="params.address">
</div>
<button class="btn btn-success" ng-click="loadUnSubs(1)"><i
class="fa fa-search"></i> Search</button>
<button class="btn btn-success pull-right" ng-click="addUnSub()" ng-click="addUnSubs()">Add</button>
</div>
</div>
</div>
<div class="box">
<div class="box-header">
<h3 class="box-title">Not Subscribed List</h3>
</div>
<div class="box-body no-padding table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th>Partner Code</th>
<th>Client Id</th>
<th>Mail Address</th>
<th>Create Time</th>
<th>Operation</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="subscribe in mailSubscribes">
<td ng-bind="subscribe.client_moniker"></td>
<td ng-bind="subscribe.client_id"></td>
<td ng-bind="subscribe.address"></td>
<td ng-bind="subscribe.create_time"></td>
<td>
<a class="text-primary" role="button" title="delete">
<i class="glyphicon glyphicon-trash" ng-click="deleteSub(subscribe.id)"></i>
</a>
</td>
</tr>
</tbody>
</table>
</div>
<div class="box-footer" ng-if="mailSubscribes.length">
<uib-pagination class="pagination"
total-items="pagination.totalCount"
boundary-links="true"
ng-model="pagination.page"
items-per-page="pagination.limit"
max-size="10"
ng-change="loadUnSubs()"
previous-text="&lsaquo;"
next-text="&rsaquo;"
first-text="&laquo;"
last-text="&raquo;"></uib-pagination>
<div class="row">
<div class="col-xs-12">Total Records:{{pagination.totalCount}};Total
Pages:{{pagination.totalPages}}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

@ -20,6 +20,11 @@
Permission Config Permission Config
</a> </a>
<a class="btn btn-app" role="button" ui-sref=".mail_subscribe">
<i class="fa fa-edit"></i>
Mail Subscriptions
</a>
<!--<a class="btn btn-app" role="button" ui-sref=".payment_config"> <!--<a class="btn btn-app" role="button" ui-sref=".payment_config">
<i class="fa fa-edit"></i> <i class="fa fa-edit"></i>
Payment Config Payment Config

@ -869,6 +869,17 @@ define(['angular', 'decimal', 'static/commons/commons', 'uiBootstrap', 'uiRouter
}) })
}) })
}; };
$scope.removeSub = function () {
$http.delete('/sys/partners/unsub/' + $scope.partner.client_moniker).then(function (resp) {
$state.reload();
});
};
$scope.addSub = function () {
$http.put('/sys/partners/unsub/' + $scope.partner.client_moniker).then(function (resp) {
$state.reload();
});
};
}]); }]);
app.controller('partnerEditCtrl', ['$scope', '$http', '$state', 'Upload', 'commonDialog', 'timezone', 'partner', app.controller('partnerEditCtrl', ['$scope', '$http', '$state', 'Upload', 'commonDialog', 'timezone', 'partner',
function ($scope, $http, $state, Upload, commonDialog, timezone, partner) { function ($scope, $http, $state, Upload, commonDialog, timezone, partner) {
@ -1068,6 +1079,7 @@ define(['angular', 'decimal', 'static/commons/commons', 'uiBootstrap', 'uiRouter
$http.get('/sys/partners/' + $scope.partner.client_moniker).then(function (resp) { $http.get('/sys/partners/' + $scope.partner.client_moniker).then(function (resp) {
$scope.paymentInfo = resp.data; $scope.paymentInfo = resp.data;
$scope.ctrl.editSubMerchant = false; $scope.ctrl.editSubMerchant = false;
$scope.ctrl.editAliSubMerchant = false;
$scope.ctrl.editMaxOrderAmount = false; $scope.ctrl.editMaxOrderAmount = false;
$scope.ctrl.editOrderExpiryConfig = false; $scope.ctrl.editOrderExpiryConfig = false;
}) })
@ -1198,6 +1210,18 @@ define(['angular', 'decimal', 'static/commons/commons', 'uiBootstrap', 'uiRouter
commonDialog.alert({title: 'Error', content: resp.data.message, type: 'error'}) commonDialog.alert({title: 'Error', content: resp.data.message, type: 'error'})
}); });
}; };
$scope.saveAliSubMerchantId = function () {
$http.put('/sys/partners/' + $scope.partner.client_moniker + '/ali_sub_merchant_id', {ali_sub_merchant_id: $scope.paymentInfo.ali_sub_merchant_id}).then(function (resp) {
commonDialog.alert({
title: 'Success',
content: 'Modify Ali Sub Merchant ID successfully',
type: 'success'
});
$scope.loadPartnerPaymentInfo();
}, function (resp) {
commonDialog.alert({title: 'Error', content: resp.data.message, type: 'error'})
});
};
$scope.refreshCredential = function () { $scope.refreshCredential = function () {
commonDialog.confirm({ commonDialog.confirm({
title: 'Warning', title: 'Warning',
@ -1210,7 +1234,7 @@ define(['angular', 'decimal', 'static/commons/commons', 'uiBootstrap', 'uiRouter
}) })
}) })
}; };
$scope.init = {jsapi: false, gateway: false, offline: false, refund: false,common_sub_merchant_id:false, channel: {}}; $scope.init = {jsapi: false, gateway: false, offline: false, refund: false,common_sub_merchant_id:false, channel: {},gateway_alipay_online:false};
$scope.switchCommonSubMerchantId = function () { $scope.switchCommonSubMerchantId = function () {
if (!$scope.paymentInfo) { if (!$scope.paymentInfo) {
return; return;
@ -1304,6 +1328,24 @@ define(['angular', 'decimal', 'static/commons/commons', 'uiBootstrap', 'uiRouter
}) })
}) })
}; };
$scope.toggleGatewayAlipayOnline = function () {
if (!$scope.paymentInfo) {
return;
}
if (!$scope.init.gateway_alipay_online) {
$scope.init.gateway_alipay_online = true;
return;
}
$http.put('/sys/partners/' + $scope.partner.client_moniker + '/gateway_alipay_online', {gateway_alipay_online: $scope.paymentInfo.gateway_alipay_online}).then(function () {
$scope.loadPartnerPaymentInfo();
}, function (resp) {
commonDialog.alert({
title: 'failed to change Gateway Alipay Online status',
content: resp.data.message,
type: 'error'
})
})
};
$scope.toggleRefund = function () { $scope.toggleRefund = function () {
if (!$scope.paymentInfo) { if (!$scope.paymentInfo) {
return; return;

@ -32,6 +32,11 @@
height: 100px; height: 100px;
margin-left: 20px; margin-left: 20px;
} }
.star_position{
position: absolute;
right: 0px;
top: -1px;
}
</style> </style>
<section class="content-header"> <section class="content-header">
<h1> <h1>
@ -515,7 +520,12 @@
</div> </div>
</div> </div>
<div class="form-group col-sm-6"> <div class="form-group col-sm-6">
<label class="control-label col-sm-4">E-mail</label> <label class="control-label col-sm-4">E-mail
<span ng-if="('10'|withRole)">
<i style="cursor: pointer" ng-click="addSub()" ng-if="!partner.unsubscribe" class="fa fa-star text-yellow star_position"></i>
<i style="cursor: pointer" ng-click="removeSub()" ng-if="partner.unsubscribe" class="fa fa-star-o text-yellow star_position"></i>
</span>
</label>
<div class="col-sm-8"> <div class="col-sm-8">
<p class="form-control-static"> <p class="form-control-static">

@ -39,6 +39,29 @@
</div> </div>
</div> </div>
</div> </div>
<div class="form-group">
<label class="col-sm-3 control-label">Ali Sub Merchant Id</label>
<div class="col-sm-9">
<p ng-if="!ctrl.editAliSubMerchant" class="form-control-static">
{{paymentInfo.ali_sub_merchant_id||'Not Configure'}}
<a role="button" ng-click="ctrl.editAliSubMerchant=true" ng-if="'10'|withRole"><i class="fa fa-edit"></i></a>
</p>
<div class="input-group" ng-if="ctrl.editAliSubMerchant">
<input type="text" class="form-control" ng-model="paymentInfo.ali_sub_merchant_id"
title="Ali Sub Merchant Id">
<div class="input-group-btn">
<button class="btn btn-success" ng-click="saveAliSubMerchantId()">
<i class="fa fa-check"></i>
</button>
</div>
<div class="input-group-btn">
<button class="btn btn-danger" ng-click="ctrl.editAliSubMerchant=false">
<i class="fa fa-remove"></i>
</button>
</div>
</div>
</div>
</div>
<div class="form-group" ng-if="'10'|withRole"> <div class="form-group" ng-if="'10'|withRole">
<label class="col-sm-3 control-label">Common Sub Merchant Id</label> <label class="col-sm-3 control-label">Common Sub Merchant Id</label>
<div class="col-xs-9"> <div class="col-xs-9">
@ -255,6 +278,12 @@
<input type="checkbox" ng-model="paymentInfo.gateway_upgrade" bs-switch switch-change="toggleGatewayUpgrade()"> <input type="checkbox" ng-model="paymentInfo.gateway_upgrade" bs-switch switch-change="toggleGatewayUpgrade()">
</div> </div>
</div> </div>
<div class="form-group">
<label class="col-sm-2 control-label">Enable Alipay Online</label>
<div class="col-sm-10">
<input type="checkbox" ng-model="paymentInfo.gateway_alipay_online" bs-switch switch-change="toggleGatewayAlipayOnline()">
</div>
</div>
<div class="form-group" ng-if="'api_surcharge'|withFunc"> <div class="form-group" ng-if="'api_surcharge'|withFunc">
<label class="col-sm-2 control-label">Customer Pay for Surcharge On Gateway</label> <label class="col-sm-2 control-label">Customer Pay for Surcharge On Gateway</label>
<div class="col-sm-10"> <div class="col-sm-10">

@ -1,10 +1,12 @@
package au.com.royalpay.payment.manage.apps.core.impls; package au.com.royalpay.payment.manage.apps.core.impls;
import au.com.royalpay.payment.manage.analysis.core.WeekReporter;
import au.com.royalpay.payment.manage.mappers.payment.OrderMapper; import au.com.royalpay.payment.manage.mappers.payment.OrderMapper;
import au.com.royalpay.payment.manage.mappers.system.ClientConfigMapper; import au.com.royalpay.payment.manage.mappers.system.ClientConfigMapper;
import au.com.royalpay.payment.manage.mappers.system.ClientMapper; import au.com.royalpay.payment.manage.mappers.system.ClientMapper;
import au.com.royalpay.payment.manage.mappers.system.OrgMapper; import au.com.royalpay.payment.manage.mappers.system.OrgMapper;
import au.com.royalpay.payment.manage.merchants.core.ClientManager; import au.com.royalpay.payment.manage.merchants.core.ClientManager;
import au.com.royalpay.payment.manage.notice.core.MailService;
import au.com.royalpay.payment.tools.mail.MailGunClient; import au.com.royalpay.payment.tools.mail.MailGunClient;
import au.com.royalpay.payment.tools.mail.SendMail; import au.com.royalpay.payment.tools.mail.SendMail;
@ -13,6 +15,12 @@ import com.alibaba.fastjson.JSONObject;
import org.apache.commons.codec.binary.Base64; import org.apache.commons.codec.binary.Base64;
import org.apache.commons.lang3.time.DateUtils; import org.apache.commons.lang3.time.DateUtils;
import org.apache.poi.hssf.usermodel.HSSFCell;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
@ -21,12 +29,15 @@ import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.net.URISyntaxException; import java.net.URISyntaxException;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date; import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@ -39,7 +50,7 @@ import cn.yixblog.platform.http.HttpRequestResult;
* Created by wangning on 05/01/2018. * Created by wangning on 05/01/2018.
*/ */
@SpringBootTest @SpringBootTest
@ActiveProfiles({ "local", "alipay", "wechat", "jd", "bestpay" }) @ActiveProfiles({ "proxy", "alipay", "wechat", "jd", "bestpay" })
@RunWith(SpringRunner.class) @RunWith(SpringRunner.class)
public class CustomerImpressionImplTest { public class CustomerImpressionImplTest {
@Resource @Resource
@ -58,6 +69,11 @@ public class CustomerImpressionImplTest {
@Resource @Resource
private ClientManager clientManager; private ClientManager clientManager;
@Resource
private MailService mailService;
@Resource
private WeekReporter weekReporter;
// @Test // @Test
// public void redisQueue() { // public void redisQueue() {
// BoundListOperations<String, String> ops = stringRedisTemplate.boundListOps("customer_impression"); // BoundListOperations<String, String> ops = stringRedisTemplate.boundListOps("customer_impression");
@ -202,4 +218,52 @@ public class CustomerImpressionImplTest {
System.out.println(asd); System.out.println(asd);
System.out.println(asd); System.out.println(asd);
} }
@Test
public void addMailUnsub() {
try {
XSSFWorkbook workbook = new XSSFWorkbook(new FileInputStream(new File("/Users/wangning/Desktop/asd.xlsx")));
XSSFSheet sheet = workbook.getSheetAt(0);
Iterator<Row> rowIterator = sheet.rowIterator();
Row row = null;
Cell cell = null;
while (rowIterator.hasNext()) {
row = rowIterator.next();
cell = row.getCell(1);
if(cell==null){
continue;
}
cell.setCellType(HSSFCell.CELL_TYPE_STRING);
CellStyle cellStyle = cell.getCellStyle();
if(cellStyle.getFillForegroundColor()==0){
continue;
}
String clientMonikers = cell.getStringCellValue().trim();
if(clientMonikers.contains("/")){
String [] clientMonikerArr = clientMonikers.split("/");
for (String s : clientMonikerArr) {
String tmp = s.trim().toUpperCase();
if(tmp.length()>4 || tmp.length()==0){
continue;
}
mailService.addUnsub(s.trim().toUpperCase());
}
}else {
String tmp = clientMonikers.trim().toUpperCase();
if(tmp.length()>4 || tmp.length()==0){
continue;
}
mailService.addUnsub(clientMonikers.trim().toUpperCase());
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
@Test
public void zxc(){
weekReporter.generateReport("2018-06-04",false);
}
} }
Loading…
Cancel
Save