1、 配额管理模块设计的思路:
a、引入账户模型。通过引入账户模型,可以通过控制配额账户的活动,我们可以允许透支,可以规定额度,也可以冻结其账户。另外系统可以追溯配额的使用情况,来龙去脉。
b、引入生产者和消费者模型。配额的产生和配额的使用是两个不同的活动。我把前一种活动产生的配额叫生产配额,后一种叫消费配额。
生产配额是有期限的,过期的生产配额是无法使用的,同时生产配额必须记录它已经使用了多少配额,这通过持有消费配额的集合 来达到目的。
与此相反,消费配额没有有效期,但是它必须记录配额在什么时候被使用。
通过这种方式,可以计算某个消费配额的剩余额度,也容易地作废某些过期配额。
2、集体实现
首先是 QuotaEntery(配额条目),做为配额增加、减少、使用、调整、作废等情况的。
/**
* 配额条目
*/
public class QuotaEntry {
/** 项目类型*/
protected QuotaEntryType entryType;
/** 额度,可正,可负 */
protected int amount;
/** 创建时间*/
private Date createTime;
protected QuotaAccount account;
public QuotaEntryType getEntryType() {
return entryType;
}
public void setEntryType(QuotaEntryType entryType) {
this.entryType = entryType;
}
public int getAmount() {
return amount;
}
public void setAmount(int amount) {
this.amount = amount;
}
public QuotaAccount getAccount() {
return account;
}
public void setAccount(QuotaAccount account) {
this.account = account;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
}
其次是ConsumeQuotaEntry(消费者)和ProduceQuotaEntry(生产者)。他们继承QuotaEntry,不同的是ProdcuceQuotaEntry记录配额的产生,ConsumeQuotaEntry则是记录配额的减少,
/**
* 生产配额 条目
*/
public class ProduceQuotaEntry extends QuotaEntry implements Comparable{
/**
* 有效期
*/
public static int DEFALUT_LIFE_YEAR = 2;
/**
* 配额的有效期
*/
private Date beginTime;
private Date endTime;
private List consumeEntries = new ArrayList(10);
public Date getBeginTime() {
return beginTime;
}
public void setBeginTime(Date beginTime) {
this.beginTime = beginTime;
}
public Date getEndTime() {
return endTime;
}
public void setEndTime(Date endTime) {
this.endTime = endTime;
}
/**
* 是否是可用的配额条目
*/
public boolean isUsable() {
Date current = new Date();
return current.getTime() >= beginTime.getTime() && current.getTime() <= endTime.getTime() &&
caclRemainQuota() > 0;
}
/**
* 是否是过期的配额
* @return
*/
public boolean isOverdue(){
return new Date().getTime() > endTime.getTime();
}
/**
* 计算当前条目剩下可用的配额
* @return
*/
public int caclRemainQuota(){
int used = 0;
for(ConsumeQuotaEntry entry:consumeEntries){
used += entry.getAmount();
}
return amount + used;
}
public int compareTo(ProduceQuotaEntry o) {
if(this.getEndTime().getTime() > o.getEndTime().getTime()){
return 1;
}
if(this.getEndTime().getTime() < o.getEndTime().getTime()){
return -1;
}
return 0;
}
public void addConsumeEntry(ConsumeQuotaEntry consume) {
consumeEntries.add(consume);
consume.setProduce(this);
}
}
/**
* 消费配额
*/
public class ConsumeQuotaEntry extends QuotaEntry {
/**减少配额 所依据的 消费配额额 */
private ProduceQuotaEntry produce;
public ProduceQuotaEntry getProduce() {
return produce;
}
public void setProduce(ProduceQuotaEntry produce) {
this.produce = produce;
}
}
QuotaEntryType,枚举类,记录 导致配额增加,减少的业务种类
public enum QuotaEntryType {
TransferFrom, //转入
TransferTo, //转出
Deduct, //扣减
Restore, //归还
Init, //初始
Decrease, //减少
Increase, //增加
Repeal; //作废
}
QuotaEntryFactory,配额条目工厂,屏蔽创建配额条目的细节
public class QuotaEntryFactory {
/**
* 返回 过期作废的配额条目
* @param amount
* @return
*/
public static ConsumeQuotaEntry getRepealEntry(int amount) throws QuotaException {
return getReducedEntry(amount,QuotaEntryType.Repeal);
}
/**
* 返回 转出的 配额
* @param amount
* @return
* @throws QuotaException
*/
public static ConsumeQuotaEntry getTransferToEntry(int amount) throws QuotaException {
return getReducedEntry(amount,QuotaEntryType.TransferTo);
}
/**
* 返回 扣减的配额
* @param amount
* @return
* @throws QuotaException
*/
public static ConsumeQuotaEntry getDeductEntry(int amount) throws QuotaException {
return getReducedEntry(amount,QuotaEntryType.Deduct);
}
/**
* 返回 减少的配额
* @param amount
* @return
* @throws QuotaException
*/
public static ConsumeQuotaEntry getDecreaseEntry(int amount) throws QuotaException {
return getReducedEntry(amount,QuotaEntryType.Decrease);
}
/**
* 返回 归还的配额
* @param consume
* @return
* @throws QuotaException
*/
public static ConsumeQuotaEntry getRestoreEntry(ConsumeQuotaEntry consume) throws QuotaException {
ConsumeQuotaEntry newConsume = new ConsumeQuotaEntry();
newConsume.setEntryType(QuotaEntryType.Restore);
newConsume.setAmount(-consume.getAmount());
newConsume.setCreateTime(new Date());
return newConsume;
}
public static ConsumeQuotaEntry getReducedEntry(int amount,QuotaEntryType type) throws QuotaException {
check(amount);
ConsumeQuotaEntry consume = new ConsumeQuotaEntry();
consume.setEntryType(type);
consume.setCreateTime(new Date());
consume.setAmount(-amount);
return consume;
}
public static ProduceQuotaEntry getInitEntry(int amount) throws QuotaException {
check(amount);
return getRaisedEntry(amount,QuotaEntryType.Init);
}
public static ProduceQuotaEntry getIncreaseQuotaEntry(int amount) throws QuotaException {
check(amount);
return getRaisedEntry(amount,QuotaEntryType.Increase);
}
public static ProduceQuotaEntry getTransferFromEntry(int amount) throws QuotaException {
check(amount);
return getRaisedEntry(amount,QuotaEntryType.TransferFrom);
}
public static ProduceQuotaEntry getRaisedEntry(int amount,QuotaEntryType type){
ProduceQuotaEntry entry = new ProduceQuotaEntry();
entry.setEntryType(type);
Calendar c = Calendar.getInstance();
entry.setBeginTime(c.getTime());
c.add(Calendar.YEAR, ProduceQuotaEntry.DEFALUT_LIFE_YEAR);
entry.setEndTime(c.getTime());
entry.setAmount(amount);
entry.setCreateTime(new Date());
return entry;
}
private static void check(int amount) throws QuotaException {
if (amount < 0) throw new QuotaException(QuotaMsg.QuotaMustBeMoreThanZero.getName());
}
}
QuotaException(配额异常消息类)
public class QuotaException extends Exception {
public QuotaException(String s) {
super(s);
}
}
QuotaMsg(各种异常消息)
public enum QuotaMsg {
AccountHasBeenFrozen {
public String getName() {
return "账户已被冻结";
}
},
AccountCantBeOverDraft {
public String getName() {
return "账户不可透支";
}
},
ItHasBeyondLimitQuota {
public String getName() {
return "已经超过透支额度";
}
},
QuotaMustBeMoreThanZero {
public String getName() {
return "配额必须大于0";
}
},
AnAccountOnlyHasAnInitEntry {
public String getName() {
return "一个账户只有一条Init记录";
}
};
public abstract String getName();
}
QuotaAccount,业务逻辑发生的地方
/**
* 配额账户..
*/
public class QuotaAccount {
/** 账户名称 */
private String name;
/** 是否冻结 */
private boolean frozen;
/** 是否可以透支 */
private boolean overdrawn;
/** 透支额度 */
private Long limit;
/** 配额条目 */
private List quotaEntries = new ArrayList(16);
/**
* 购买商品后,扣减配额
* @param amount 额度
*/
public List deduct(int amount) throws QuotaException {
checkAccount(amount);
//可用的生产配额
List avaiEntries = findAvaiEntry();
//生成的消费配额
List consumeList = reduceQuota(avaiEntries,amount,QuotaEntryType.Deduct);
return consumeList;
}
/**
* 计算剩余配额
* @return
*/
public int caclOddQuota(){
int count = 0;
for(QuotaEntry entry:quotaEntries){
count += entry.getAmount();
}
return count;
}
/**
* 归还配额
* @param consume 消费配额
* @throws QuotaException
*/
private void restore(ConsumeQuotaEntry consume) throws QuotaException {
//1、找到对应的生产积分条目
ProduceQuotaEntry produce = consume.getProduce();
//2、生成一条 归还条目
ConsumeQuotaEntry newConsume = QuotaEntryFactory.getRestoreEntry(consume);
//3、把积分写到指定生产配额下
produce.addConsumeEntry(newConsume);
//4、添加到账户下
addQuotaEntry(newConsume);
}
/**
* 归还配额
* @param consumeList 消费条目列表
* @throws QuotaException
*/
public void restore(List consumeList) throws QuotaException {
for(ConsumeQuotaEntry consume:consumeList){
restore(consume);
}
}
/**
* 作废生产配额
* @param produce 生产配额
* @return
* @throws QuotaException
*/
public ConsumeQuotaEntry repeal(ProduceQuotaEntry produce) throws QuotaException {
int remain = produce.caclRemainQuota();
ConsumeQuotaEntry consume = QuotaEntryFactory.getRepealEntry(remain);
produce.addConsumeEntry(consume);
addQuotaEntry(consume);
return consume;
}
/**
* 作废所有过期的配额
* @return
*/
public void repeal() throws QuotaException {
for(QuotaEntry entry:quotaEntries) {
if(ProduceQuotaEntry.class.isInstance(entry)){
ProduceQuotaEntry produce = (ProduceQuotaEntry)entry;
if(produce.isOverdue() && produce.caclRemainQuota() > 0){
repeal(produce);
}
}
}
}
/**
* 把 一定数量的配额 转移到 另一个账户上
*
* @param account 目标账户
* @param amount 额度
* @throws QuotaException
*/
public void transferTo(QuotaAccount account, int amount) throws QuotaException {
checkAccount(amount);
List avaiEntries = findAvaiEntry();
List consumeList= reduceQuota(avaiEntries,amount,QuotaEntryType.TransferTo);
account.transferFrom(consumeList);
}
/**
* 接收 从某个账户转移过来的额度
*
*/
public void transferFrom(List consumeList) throws QuotaException {
int amount = 0;
for(ConsumeQuotaEntry consume:consumeList){
amount += consume.getAmount();
}
amount = Math.abs(amount);
transferFrom(amount);
}
private void transferFrom(int amount) throws QuotaException {
ProduceQuotaEntry produce = QuotaEntryFactory.getTransferFromEntry(amount);
addQuotaEntry(produce);
}
/**
* 增加 配额
*
* @param amount
*/
public ProduceQuotaEntry increaseQuota(int amount) throws QuotaException {
ProduceQuotaEntry produce = QuotaEntryFactory.getIncreaseQuotaEntry(amount);
addQuotaEntry(produce);
return produce;
}
/**
* 减少 配额
* produce 生产配额
* @param amount
*/
public void decreaseQuota(ProduceQuotaEntry produce,int amount) throws QuotaException {
checkAccount(amount);
if(produce.caclRemainQuota() findAvaiEntry(){
List avaiEntries = new ArrayList(10);
for(QuotaEntry entry:quotaEntries){
if(!ProduceQuotaEntry.class.isInstance(entry)) break;
ProduceQuotaEntry produce = (ProduceQuotaEntry) entry;
if(produce.caclRemainQuota() > 0 && produce.isUsable()){
avaiEntries.add(produce);
}
}
Collections.sort(avaiEntries);
return avaiEntries;
}
/**
* 创建初始配额账户
*
* @param amount
*/
public void init(int amount) throws QuotaException {
if (hasInitEntry()) throw new QuotaException(QuotaMsg.AnAccountOnlyHasAnInitEntry.getName());
addQuotaEntry(QuotaEntryFactory.getInitEntry(amount));
}
public boolean hasInitEntry() {
boolean has = false;
for (QuotaEntry entry : quotaEntries) {
if (entry.getEntryType().equals(QuotaEntryType.Init)) {
has = true;
break;
}
}
return has;
}
/**
* 从一群 可用的而生产配额中,减去 配额
* @param avaiEntries
* @param amount
* @return 扣减记录
* @throws QuotaException
*/
private List reduceQuota(List avaiEntries,int amount,QuotaEntryType type) throws QuotaException {
List consumeList= new ArrayList(4);
int remain =amount;
int toReduce = amount;
Iterator iterator = avaiEntries.iterator();
while(iterator.hasNext()){
ProduceQuotaEntry produce = iterator.next();
remain -= produce.caclRemainQuota();
ConsumeQuotaEntry consume= null;
//如果还没有扣完,而且还有配额条目
if(remain > 0 && iterator.hasNext()){
consume = QuotaEntryFactory.getReducedEntry(produce.getAmount(),type);
toReduce = amount - produce.getAmount();
//如果没有扣完,也没有其他配额条目,
}else if(remain > 0 && !iterator.hasNext()){
consume = QuotaEntryFactory.getDeductEntry(toReduce);
}else if(remain <= 0){
consume = QuotaEntryFactory.getDeductEntry(toReduce);
}
produce.addConsumeEntry(consume);
addQuotaEntry(consume);
consumeList.add(consume);
}
return consumeList;
}
/**
* 检验配额是是否足够
* @param amount
* @throws QuotaException
*/
private void checkAccount(int amount) throws QuotaException {
if (isFrozen()) throw new QuotaException(QuotaMsg.AccountHasBeenFrozen.getName());
if (!isOverdrawn() && caclOddQuota() < amount)
throw new QuotaException(QuotaMsg.AccountCantBeOverDraft.getName());
if (isOverdrawn() && (caclOddQuota() + limit < amount))
throw new QuotaException(QuotaMsg.ItHasBeyondLimitQuota.getName());
}
/**
* 添加配额条目
* @param quotaEntry
*/
private void addQuotaEntry(QuotaEntry quotaEntry){
quotaEntries.add(quotaEntry);
quotaEntry.setAccount(this);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public boolean isFrozen() {
return frozen;
}
public void setFrozen(boolean frozen) {
this.frozen = frozen;
}
public boolean isOverdrawn() {
return overdrawn;
}
public void setOverdrawn(boolean overdrawn) {
this.overdrawn = overdrawn;
}
public Long getLimit() {
return limit;
}
public void setLimit(Long limit) {
this.limit = limit;
}
public List getQuotaEntries() {
return quotaEntries;
}
public void setQuotaEntries(List quotaEntries) {
this.quotaEntries = quotaEntries;
}
}
测试类
public class QuotaAccountTest {
private QuotaAccount account;
private QuotaAccount accountTwo;
@Before
public void setUp() throws QuotaException, ParseException {
account = new QuotaAccount();
account.increaseQuota(34);
account.setFrozen(false);
account.setLimit(10L);
account.setOverdrawn(true);
account.setName("小明的账户");
accountTwo = new QuotaAccount();
accountTwo.increaseQuota(34);
accountTwo.setFrozen(false);
accountTwo.setLimit(10L);
accountTwo.setOverdrawn(true);
accountTwo.setName("小明的账户2");
}
@After
public void tearDown(){
account =null;
accountTwo = null;
}
@Test
public void caclRemainQuota(){
assertEquals(34, account.caclOddQuota());
}
@Test
public void caclRemainQuotaAfterDeducting4Quotas() throws QuotaException {
account.deduct(4);
assertEquals(30,account.caclOddQuota());
}
@Test
public void caclRemainQuotaAfterDeducting44Quotas() throws QuotaException {
account.deduct(44);
assertEquals(-10,account.caclOddQuota());
}
@Test(expected = QuotaException.class)
public void caclRemainQuotaAfterDeductingAndAccountItIsNotOverdrawn() throws QuotaException {
account.setOverdrawn(false);
account.deduct(44);
}
@Test
public void caclRemainQuotaAfterRestoreQuota() throws QuotaException {
List consumes =account.deduct(44);
assertEquals(-10,account.caclOddQuota());
account.restore(consumes);
assertEquals(34,account.caclOddQuota());
}
@Test
public void caclRemainQuotaAferIncreasingQuotaAndDecutingQuota() throws QuotaException {
account.increaseQuota(20);
List consumes =account.deduct(44);
assertEquals(10,account.caclOddQuota());
}
@Test
public void transferSmallQuotas() throws QuotaException {
account.transferTo(accountTwo,12);
assertEquals(22,account.caclOddQuota());
assertEquals(46,accountTwo.caclOddQuota());
}
@Test
public void transferMiddleQuotas() throws QuotaException {
account.transferTo(accountTwo,34);
assertEquals(0,account.caclOddQuota());
assertEquals(68,accountTwo.caclOddQuota());
}
@Test
public void transferLargeQuotas() throws QuotaException {
account.transferTo(accountTwo,44);
assertEquals(-10,account.caclOddQuota());
assertEquals(78,accountTwo.caclOddQuota());
}
@Test
public void transferLargeLargeQuota() throws QuotaException {
account.increaseQuota(30);
account.transferTo(accountTwo,44);
assertEquals(20,account.caclOddQuota());
assertEquals(78,accountTwo.caclOddQuota());
}
@Test
public void testRepealEntry() throws QuotaException {
ProduceQuotaEntry produce = new ProduceQuotaEntry();
produce.setAmount(34);
ConsumeQuotaEntry consume =account.repeal(produce);
assertEquals(0, produce.getAmount() + consume.getAmount());
}
}