출처 : http://woowabros.github.io/experience/2018/10/12/new_point_story_1.html
위 출처를 보면 우아한형제들에서 포인트 시스템을 구축할 때 생각했던 점들이 나온다.
나도 어느 순간 포인트 시스템을 구현해야 했는데 많은 도움을 받았다.
위 도표를 보고 DB로만 구현한 내용을 공유해보고자 한다.
MariaDB 10.4 버전을 사용하였으나 어지간하면 다 동작할 것이다.
-- 사용자
CREATE TABLE IF NOT EXISTS `users` (
`Id` binary(16) NOT NULL,
`Name` varchar(50) NOT NULL,
PRIMARY KEY (`Id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 포인트 이벤트
CREATE TABLE IF NOT EXISTS `point_event` (
`Id` binary(16) NOT NULL,
`UserId` binary(16) NOT NULL,
`Type` char(1) NOT NULL DEFAULT 'A',
`Point` int(11) NOT NULL DEFAULT 0,
`Comment` varchar(64) NOT NULL DEFAULT '',
`ExpireDate` date DEFAULT NULL,
`CreateDate` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`Id`),
KEY `FK_point_event_userId_users_id` (`UserId`),
CONSTRAINT `FK_point_event_userId_users_id` FOREIGN KEY (`UserId`)
REFERENCES `users` (`Id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 포인트 상세
CREATE TABLE IF NOT EXISTS `point_detail` (
`Id` binary(16) NOT NULL,
`PointId` binary(16) NOT NULL,
`GroupId` binary(16) NOT NULL,
`Type` char(1) NOT NULL DEFAULT 'A',
`Point` int(11) NOT NULL DEFAULT 0,
`CreateDate` timestamp NOT NULL DEFAULT current_timestamp(),
PRIMARY KEY (`Id`),
KEY `FK_point_detail_PointId_reward_point_id` (`PointId`),
CONSTRAINT `FK_point_detail_PointId_reward_point_id` FOREIGN KEY (`PointId`)
REFERENCES `point_event` (`Id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- 사용한 포인트를 취소하는 프로시저
DELIMITER //
CREATE DEFINER=`root`@`localhost` PROCEDURE `sp_point_use_cancel`(
IN iPointId BINARY(16),
IN iComment VARCHAR(64)
)
COMMENT '포인트 사용 취소'
BEGIN
DECLARE mNewPointId BINARY(16);
DECLARE mType char(1);
SET mNewPointId = fn_create_key();
-- U 타입만 취소 가능함
select `Type` into mType
FROM point_event
WHERE Id = iPointId;
if mType = 'U' then
INSERT INTO point_event(Id, UserId, `Type`, `Point`, `Comment`)
select mNewPointId, UserId, 'C', (`Point` * -1), iComment
from point_event
where Id = iPointId;
INSERT INTO point_detail(Id, PointId, `Type`, `Point`, GroupId)
select fn_create_key(), mNewPointId, 'C', (`Point` * -1), GroupId
from point_detail
WHERE PointId = iPointId;
end if;
END//
DELIMITER ;
-- 만료일이 지난 포인트 정리하는 프로시저
DELIMITER //
CREATE DEFINER=`root`@`localhost` PROCEDURE `sp_point_expire`(
IN `iWorkDate` DATE
)
COMMENT '포인트 만료 정리'
BEGIN
DECLARE mDone INT DEFAULT 0;
DECLARE mUserId BINARY(16);
DECLARE mGroupId BINARY(16);
DECLARE mPoint INT;
DECLARE mPointId BINARY(16);
DECLARE mDetailId BINARY(16);
DECLARE cur CURSOR FOR
SELECT t.GroupId, t.`Point`
FROM (
SELECT d.GroupId, SUM(d.`Point`) AS `Point`
FROM point_event AS p
JOIN point_detail AS d ON p.Id = d.PointId
WHERE p.ExpireDate < iWorkDate OR p.ExpireDate IS NULL
GROUP BY d.GroupId
) AS t
WHERE `Point` > 0;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET mDone = 1;
OPEN cur;
read_loop : LOOP
FETCH cur INTO mGroupId, mPoint;
IF mDone THEN
LEAVE read_loop;
END IF;
SELECT p.UserId INTO mUserId
FROM point_detail AS d
JOIN point_event AS p ON d.PointId = p.Id
WHERE d.GroupId = mGroupId
LIMIT 1;
SET mPointId = fn_create_key();
INSERT INTO point_event(Id, UserId, `Type`, `Point`, `Comment`)
VALUES(mPointId, mUserId, 'E', (mPoint * -1), '유효기간만료');
INSERT INTO point_detail(Id, PointId, `Type`, `Point`, GroupId)
VALUES(fn_create_key(), mPointId, 'E', (mPoint * -1), mGroupId);
END LOOP;
CLOSE cur;
END//
DELIMITER ;
-- 포인트 적립하는 프로시저
DELIMITER //
CREATE DEFINER=`root`@`localhost` PROCEDURE `sp_point_add`(
IN iUserId binary(16),
IN iPoint Int,
IN iExpireDate Date,
IN iComment varchar(64)
)
COMMENT '포인트 적립'
BEGIN
declare mPointId binary(16);
DECLARE mDetailId BINARY(16);
set mPointId = fn_create_key();
SET mDetailId = fn_create_key();
insert into point_event(Id, UserId, `Point`, `Comment`, ExpireDate)
values(mPointId, iUserId, iPoint, iComment, iExpireDate);
INSERT INTO point_detail(Id, PointId, `Point`, GroupId)
values(mDetailId, mPointId, iPoint, mDetailId);
END//
DELIMITER ;
-- 포인트 사용하는 프로시저
DELIMITER //
CREATE DEFINER=`root`@`localhost` PROCEDURE `sp_point_use`(
IN iUserId BINARY(16),
IN iPoint INT,
IN iComment VARCHAR(64),
OUT oResultCode char(1),
out oPointId BINARY(16)
)
COMMENT '포인트 사용'
BEGIN
/*
* oResultCode : A-승인, F-잔여포인트 부족, E-Exception
*/
DECLARE mDone INT DEFAULT 0;
DECLARE mPointId BINARY(16);
DECLARE mDetailId BINARY(16);
DECLARE mGroupId BINARY(16);
Declare mTotalPoint INT;
declare mCurrentPoint INT;
DECLARE mPoint INT;
DECLARE cur CURSOR FOR
SELECT t.GroupId, t.`Point`
FROM (
SELECT d.GroupId, SUM(d.`Point`) AS `Point`
FROM point_event AS p
JOIN point_detail AS d ON p.Id = d.PointId
WHERE p.UserId = iUserId
-- AND p.ExpireDate >= CURRENT_DATE
GROUP BY d.GroupId
ORDER BY p.ExpireDate
) AS t
WHERE `Point` > 0;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET mDone = 1;
/* 만약 SQL에러라면 ROLLBACK 처리한다. */
DECLARE exit handler for SQLEXCEPTION
BEGIN
SET oResultCode = 'E';
ROLLBACK;
END;
set mCurrentPoint = iPoint;
START TRANSACTION;
-- 총 포인트를 구해본다
SELECT IFNULL(SUM(`point`), 0) INTO mTotalPoint
FROM users AS u
left join point_event AS p ON u.Id = p.UserId
WHERE u.Id = iUserId
GROUP BY p.UserId;
if(mTotalPoint < iPoint) then
set oResultCode = 'F';
else
-- point 를 먼저 넣고
SET mPointId = fn_create_key();
INSERT INTO point_event(Id, UserId, `Type`, `Point`, `Comment`)
VALUES(mPointId, iUserId, 'U', (iPoint * -1), iComment);
OPEN cur;
read_loop : LOOP
FETCH cur INTO mGroupId, mPoint;
IF mDone OR mCurrentPoint < 1 THEN
LEAVE read_loop;
END IF;
SET mDetailId = fn_create_key();
if mCurrentPoint > mPoint then
-- 빼야하는게 더 크면 통째로 뺀다
INSERT INTO point_detail(Id, PointId, `Type`, `Point`, GroupId)
VALUES(mDetailId, mPointId, 'U', (mPoint * -1), mGroupId);
set mCurrentPoint = mCurrentPoint - mPoint;
else
-- 빼야하는게 같거나 작다면 일부만 뺀다
INSERT INTO point_detail(Id, PointId, `Type`, `Point`, GroupId)
VALUES(mDetailId, mPointId, 'U', (mCurrentPoint * -1), mGroupId);
SET mCurrentPoint = 0;
end if;
END LOOP;
CLOSE cur;
-- 결과값 반환
SET oResultCode = 'A';
set oPointId = mPointId;
end If;
COMMIT;
END//
DELIMITER ;
-- binary key 생성
DELIMITER //
CREATE DEFINER=`root`@`%` FUNCTION `fn_create_key`() RETURNS binary(16)
COMMENT '순차적 BINARY ID 생성'
BEGIN
SET @UUID = UUID();
RETURN
UNHEX(CONCAT(
SUBSTR(@UUID, 15, 4),
SUBSTR(@UUID, 10, 4),
SUBSTR(@UUID, 1, 8),
SUBSTR(@UUID, 20, 4),
SUBSTR(@UUID, 25) ));
END//
DELIMITER ;