积分商城系统设计

随着互联网、移动联网、大数据的快速发展,积分商城系统的应用也是越来越广泛啦,各种购物都有会与各种的积分有一定的关联。

积分商城系统架构要完成一个积分运作的闭环,即:商家发放积分,消费者用户获得积分,用户利用积分兑换商品,礼品,优惠券等,积分再次回到商家或平台形成这么一个循环的闭环。

系统特点

1、制定完善完整的积分兑换规则,提高兑换订单处理效率,增加适量低积分商品,礼品等。吸引消费者积极使用积分,提高积分商品的吸引度,增强消费者粘度。

2、整个积分商城的积分兑换尽量线上完成,如:消费者用积分兑换商品、礼品,线上生成订单,使用快递物流进行发货,直接送到客户手中。无需到线下实体店领取。

3、有效控制商品,礼品的兑换有期间,尽量使用商品,礼品长期都可以持续兑换,避免搞运动式的兑换活动。

系统设计

1、店铺创建活动,通过活动促销兑换商品。活动会定义规则。用户满足活动规则才允许兑换商品。活动规则的匹配我们 使用QLExpress规则引擎,详情设计参考后面代码。

2、分布式ID,前期采用数据库方式 生成ID (不采用自增长ID),为以后数据库拆分准备。生成方式参考后面代码。

3、积分兑换商品需要生成兑换单,对订单数据需要进行分表处理,目前订单表根据主键ID分表,共256张表。

4、对订单数据的查询请求,使用ES 方式查询。

5、定义CommandHandler 模版,统一业务代码处理风格。参考后面代码。

 QLExpress

一个轻量级的类java语法规则引擎,作为一个嵌入式规则引擎在业务系统中使用。让业务规则定义简便而不失灵活。让业务人员就可以定义业务规则。支持标准的JAVA语法,还可以支持自定义操作符号、操作符号重载、函数定义、宏定义、数据延迟加载等。https://github.com/alibaba/QLExpress

积分商城系统设计_第1张图片

POM坐标

        
        
            com.alibaba
            QLExpress
            3.2.0
        

QLExpressContext 定义

public class QLExpressContext extends HashMap implements IExpressContext {
    private ApplicationContext applicationContext;

    public QLExpressContext(Map properties, ApplicationContext context) {
        super(properties);
        this.applicationContext = context;
    }

    @Override
    public Object get(Object name) {
        Object result;
        result = super.get(name);
        try {
            if (result == null && this.applicationContext != null && this.applicationContext.containsBean((String) name)) {

                result = this.applicationContext.getBean((String) name);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return result;
    }

    @Override
    public Object put(String name, Object object) {
        super.put(name, object);
        return object;
    }
}

QLExpressHandler 定义 

@Component
public class QLExpressHandler implements InitializingBean, ApplicationContextAware {

    private ExpressRunner runner;

    private ApplicationContext applicationContext;

    public Object execute(String statement, Map context) throws Exception {
        IExpressContext expressContext = new QLExpressContext(context != null ? context : Collections.EMPTY_MAP, applicationContext);
        return runner.execute(statement, expressContext, null, true, false);
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        runner = new ExpressRunner(false, false);

        Map beanMap = applicationContext.getBeansOfType(RuleHandler.class);

        beanMap.values().forEach(bean -> {
            Method[] methods = bean.getClass().getDeclaredMethods();

            for (Method method : methods) {
                QlRule qlRule = method.getAnnotation(QlRule.class);

                try {
                    runner.addFunctionOfClassMethod(qlRule.methodName(), bean.getClass().getName(), method.getName(),
                            method.getParameterTypes(), null);
                } catch (Exception ex) {

                }

            }
        });
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

 规则类定义

积分商城系统设计_第2张图片

public interface RuleHandler {
}
/**
 * QLRule 注解,Spring启动时扫描
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface QlRule {

    /**
     * 方法名称
     */
    String methodName();

    /**
     * 方法描述
     */
    String desc() default "";
}
@Component
public class Activity implements RuleHandler {

    @QlRule(methodName = "checkUserName", desc = "")
    public boolean checkUserName(String name) {
        return Objects.equals("yangyanping", name);
    }
}

 单元测试

public class QlTest extends BootBaseTest {

    @Autowired
    private QLExpressHandler qlExpressHandler;

    @Test
    public void testQl() throws Exception {

        String script = "com.ql.Activity; return checkUserName(name);";

        Map context = new HashMap<>();
        context.put("name", "xiyangyang");
        Object result = qlExpressHandler.execute(script, context);
        System.out.println("xiyangyang=" + result);

        context.put("name", "yangyanping");
        result = qlExpressHandler.execute(script, context);
        System.out.println("yangyanping=" + result);
    }
}

输出结果

xiyangyang=false
yangyanping=true

分表 实现

 DbTableUtil 计算分表的index 

public class DbTableUtil {
    private static final int TABLE_SIZE = 256;

    public static String getTableIndex(Object shardKey, int sliceSize) {
        int hashValue = Math.abs(shardKey.hashCode());
        int tableIndex = hashValue % sliceSize + 1;

        // 对整数进行格式化 占位符格式为: %[index$][标识]*[最小宽度]转换符
        return String.format("%04d", tableIndex);
    }

    public static String hash(long key, int sliceSize) {
        long hashValue = Math.abs(key);
        int tableIndex = (int) (hashValue % sliceSize + 1);

        // 对整数进行格式化 占位符格式为: %[index$][标识]*[最小宽度]转换符
        return String.format("%04d", tableIndex);
    }

    public static String hash(long key) {
        return hash(key, TABLE_SIZE);
    }
}

分布式ID 

 sequence表

字段 name 保存数据表的名称,字段value 保存表当前最大ID。

CREATE TABLE `sys`.`sequence` (
  `name` VARCHAR(64) NOT NULL COMMENT '表名称',
  `value` BIGINT(20) NOT NULL COMMENT '当前值',
  PRIMARY KEY (`name`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COMMENT = 'sequence 表';

SequenceUtil 设计

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;

import lombok.Data;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

@Data
public class Sequence {
    private static final Log log = LogFactory.getLog(Sequence.class);

    /**
     * 容量大小
     */
    private int blockSize = 100;

    /**
     * 初始值大小
     */
    private long startValue = 0L;

    /**
     * 数据源
     */
    private DataSource dataSource;

    private Map stepMap = new HashMap<>();

    public Sequence() {
    }

    public synchronized long get(String sequenceName) {
        Step step = this.stepMap.get(sequenceName);
        if (step == null) {
            step = new Sequence.Step(this.startValue, this.startValue + (long) this.blockSize);
            this.stepMap.put(sequenceName, step);
        } else if (step.currentValue < step.endValue) {
            return step.incrementAndGet();
        }

        for (int i = 0; i < this.blockSize; ++i) {
            if (this.getNextBlock(sequenceName, step)) {
                return step.incrementAndGet();
            }
        }

        throw new RuntimeException("get error.");
    }

    private boolean getNextBlock(String sequenceName, Step step) {
        Long value = this.getPersistenceValue(sequenceName);
        if (value == null) {
            try {
                value = this.newPersistenceValue(sequenceName);
            } catch (Exception var5) {
                log.error("newPersistenceValue error!");
                value = this.getPersistenceValue(sequenceName);
            }
        }

        boolean b = this.saveValue(value, sequenceName) == 1;
        if (b) {
            step.setCurrentValue(value);
            step.setEndValue(value + (long) this.blockSize);
        }

        return b;
    }

    private int saveValue(long value, String sequenceName) {
        Connection connection = null;
        PreparedStatement statement = null;

        int var8;
        try {
            connection = this.dataSource.getConnection();
            statement = connection.prepareStatement("update sequence_value set value = ?  where name = ? and value = ?");
            statement.setLong(1, value + (long) this.blockSize);
            statement.setString(2, sequenceName);
            statement.setLong(3, value);
            var8 = statement.executeUpdate();
        } catch (Exception var18) {
            log.error("newPersistenceValue error!", var18);
            throw new RuntimeException("newPersistenceValue error!", var18);
        } finally {
            this.close(null,statement, connection);
        }

        return var8;
    }

    private Long getPersistenceValue(String sequenceName) {
        Connection connection = null;
        PreparedStatement statement = null;
        ResultSet resultSet = null;

        try {
            connection = this.dataSource.getConnection();
            statement = connection.prepareStatement("select value from sequence_value where name = ?");
            statement.setString(1, sequenceName);
            resultSet = statement.executeQuery();
            if (resultSet.next()) {
                Long value = resultSet.getLong("value");
                return value;
            }
        } catch (Exception var23) {
            log.error("getPersistenceValue error!", var23);
            throw new RuntimeException("getPersistenceValue error!", var23);
        } finally {
            this.close(resultSet,statement, connection);
        }

        return null;
    }

    private Long newPersistenceValue(String sequenceName) {
        Connection connection = null;
        PreparedStatement statement = null;

        try {
            connection = this.dataSource.getConnection();
            statement = connection.prepareStatement("insert into sequence_value (value,name) values (?,?)");
            statement.setLong(1, this.startValue);
            statement.setString(2, sequenceName);
            statement.executeUpdate();
        } catch (Exception var15) {
            log.error("newPersistenceValue error!", var15);
            throw new RuntimeException("newPersistenceValue error!", var15);
        } finally {
            this.close(null,statement, connection);
        }

        return this.startValue;
    }

    private void close(ResultSet resultSet, PreparedStatement statement, Connection connection) {
        if (resultSet != null) {
            try {
                resultSet.close();
            } catch (SQLException var22) {
                log.error("close resultset error!", var22);
            }
        }

        if (statement != null) {
            try {
                statement.close();
            } catch (SQLException var14) {
                log.error("close statement error!", var14);
            }
        }

        if (connection != null) {
            try {
                connection.close();
            } catch (SQLException var13) {
                log.error("close connection error!", var13);
            }
        }
    }

    /**
     * 步长定义
     */
    @Data
    static class Step {
        /**
         * 初始值
         */
        private long currentValue;

        /**
         * 截止值
         */
        private long endValue;

        Step(long currentValue, long endValue) {
            this.currentValue = currentValue;
            this.endValue = endValue;
        }

        public long incrementAndGet() {
            return ++this.currentValue;
        }
    }
}
@Data
public class SequenceUtil {
    private Map sequenceMap;
    private Sequence defaultSequence;

    public long get(String name) {
        Sequence sequence = null;
        if (this.sequenceMap != null) {
            sequence = this.sequenceMap.get(name);
        }

        if (sequence == null) {
            if (this.defaultSequence != null) {
                return this.defaultSequence.get(name);
            } else {
                throw new RuntimeException("sequence " + name + " undefined!");
            }
        } else {
            return sequence.get(name);
        }
    }
}

CommandHandler 模版类定义

积分商城系统设计_第3张图片

 ApiResult

import lombok.Getter;
import lombok.Setter;

import java.util.Objects;

/**
 * 结果包装类定义
 * @author yangyanping
 * @date 2022-08-01
 */
public class ApiResult {
    private final String SUCCESS_CODE = "0000";

    /**
     * 码表
     */
    @Getter
    @Setter
    private String code;

    /**
     * 错误信息
     */
    @Getter
    @Setter
    private String errMsg;

    /**
     * 数据
     */
    @Getter
    @Setter
    private T data;

    public ApiResult success() {
        return success(null);
    }

    public ApiResult success(T data) {
        ApiResult apiResult = new ApiResult<>();
        apiResult.setCode(SUCCESS_CODE);
        apiResult.setData(data);

        return apiResult;
    }

    public ApiResult failuer(String code, String errMsg) {
        ApiResult apiResult = new ApiResult<>();
        apiResult.setCode(code);
        apiResult.setErrMsg(errMsg);

        return apiResult;
    }

    /**
     * 是否成功
     */
    public boolean checkSuccess() {
        return Objects.equals(SUCCESS_CODE, this.code);
    }
}

CommandHandler 类定义 

/**
 * CommandHandler 接口定义
 * @param 

请求参数 * @param 请求接口 * @param 请求头 */ public interface CommandHandler { /** * 业务处理器 */ ApiResult doHandler(P param, H header); }

AbstractCommandHandler 模版处理器定义

/**
 * 抽象处理器类定义
 * @author yangyanping
 * @date 2022-08-01
 */
public abstract class AbstractCommandHandler implements CommandHandler {
    /**
     * 执行处理器
     */
    @Override
    public ApiResult doHandler(P param, H header) {
        ApiResult apiResult = new ApiResult<>();
        long startTime = System.currentTimeMillis();

        try {
            this.checkHeader(header);
            this.checkParam(param);

            R result = this.doExecute(param, header);
            apiResult.setData(result);
            apiResult.setCode(ApiResult.SUCCESS_CODE);
        } catch (Exception ex) {
            apiResult.setCode("0001");
            apiResult.setErrMsg(ex.getMessage());
        } finally {
            System.out.println("cost time " + (System.currentTimeMillis() - startTime) + "ms");
        }

        return apiResult;
    }

    /**
     * 检查参数
     */
    protected abstract void checkParam(P param);

    /**
     * 检查header
     */
    protected abstract void checkHeader(H header);

    /**
     * 执行
     */
    protected abstract R doExecute(P param, H header);
}

AbstractShopCommandHandler 增加token验证

/**
 * 商城处理器类定义
 */
public abstract class AbstractShopCommandHandler extends AbstractCommandHandler {

    /**
     * 验证权限
     */
    protected void checkHeader(Protocol header) {
        /// todo 从表system_toke_config 验证 appName 和 token
    }
}

系统token 表

CREATE TABLE `sys`.`system_toke_config` (
  `id` INT UNSIGNED NOT NULL COMMENT '主键',
  `app_name` VARCHAR(64) NOT NULL COMMENT '应用名称',
  `token` VARCHAR(128) NOT NULL COMMENT '请求token',
  `remark` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '备注',
  PRIMARY KEY (`id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8
COMMENT = '系统token配置表';

你可能感兴趣的:(分布式,MySQL,mysql,数据库)