随着互联网、移动联网、大数据的快速发展,积分商城系统的应用也是越来越广泛啦,各种购物都有会与各种的积分有一定的关联。
积分商城系统架构要完成一个积分运作的闭环,即:商家发放积分,消费者用户获得积分,用户利用积分兑换商品,礼品,优惠券等,积分再次回到商家或平台形成这么一个循环的闭环。
系统特点
1、制定完善完整的积分兑换规则,提高兑换订单处理效率,增加适量低积分商品,礼品等。吸引消费者积极使用积分,提高积分商品的吸引度,增强消费者粘度。
2、整个积分商城的积分兑换尽量线上完成,如:消费者用积分兑换商品、礼品,线上生成订单,使用快递物流进行发货,直接送到客户手中。无需到线下实体店领取。
3、有效控制商品,礼品的兑换有期间,尽量使用商品,礼品长期都可以持续兑换,避免搞运动式的兑换活动。
1、店铺创建活动,通过活动促销兑换商品。活动会定义规则。用户满足活动规则才允许兑换商品。活动规则的匹配我们 使用QLExpress规则引擎,详情设计参考后面代码。
2、分布式ID,前期采用数据库方式 生成ID (不采用自增长ID),为以后数据库拆分准备。生成方式参考后面代码。
3、积分兑换商品需要生成兑换单,对订单数据需要进行分表处理,目前订单表根据主键ID分表,共256张表。
4、对订单数据的查询请求,使用ES 方式查询。
5、定义CommandHandler 模版,统一业务代码处理风格。参考后面代码。
一个轻量级的类java语法规则引擎,作为一个嵌入式规则引擎在业务系统中使用。让业务规则定义简便而不失灵活。让业务人员就可以定义业务规则。支持标准的JAVA语法,还可以支持自定义操作符号、操作符号重载、函数定义、宏定义、数据延迟加载等。https://github.com/alibaba/QLExpress
POM坐标
com.alibaba
QLExpress
3.2.0
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;
}
}
@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;
}
}
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);
}
}
字段 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 表';
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);
}
}
}
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);
}
/**
* 抽象处理器类定义
* @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);
}
/**
* 商城处理器类定义
*/
public abstract class AbstractShopCommandHandler extends AbstractCommandHandler
{
/**
* 验证权限
*/
protected void checkHeader(Protocol header) {
/// todo 从表system_toke_config 验证 appName 和 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配置表';