2023-08-25 12:31:32 +09:00
|
|
|
package core
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/binary"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
|
|
|
|
"math/rand"
|
|
|
|
|
"net/http"
|
|
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"go.mongodb.org/mongo-driver/bson"
|
|
|
|
|
"go.mongodb.org/mongo-driver/bson/primitive"
|
|
|
|
|
"go.mongodb.org/mongo-driver/mongo/options"
|
|
|
|
|
"repositories.action2quare.com/ayo/gocommon"
|
|
|
|
|
coupon "repositories.action2quare.com/ayo/gocommon/coupon"
|
|
|
|
|
"repositories.action2quare.com/ayo/gocommon/logger"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
CollectionCoupon = gocommon.CollectionName("coupon")
|
|
|
|
|
CollectionCouponUse = gocommon.CollectionName("coupon_use")
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type couponDoc struct {
|
|
|
|
|
Name string `json:"name" bson:"name"`
|
|
|
|
|
Effect string `json:"effect" bson:"effect"`
|
|
|
|
|
Desc string `json:"desc" bson:"desc"`
|
|
|
|
|
Total int64 `json:"total" bson:"total"`
|
|
|
|
|
Remains []string `json:"remains,omitempty" bson:"remains,omitempty"`
|
|
|
|
|
Used []string `json:"used,omitempty" bson:"used,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func makeCouponKey(roundnum uint32, uid []byte) string {
|
|
|
|
|
left := binary.BigEndian.Uint16(uid[0:2])
|
|
|
|
|
right := binary.BigEndian.Uint16(uid[2:4])
|
|
|
|
|
multi := uint32(left) * uint32(right)
|
|
|
|
|
xor := roundnum ^ multi
|
|
|
|
|
|
|
|
|
|
final := make([]byte, 8)
|
|
|
|
|
binary.LittleEndian.PutUint32(final, xor)
|
|
|
|
|
copy(final[4:], uid)
|
|
|
|
|
return fmt.Sprintf("%s-%s-%s-%s", hex.EncodeToString(final[0:2]), hex.EncodeToString(final[2:4]), hex.EncodeToString(final[4:6]), hex.EncodeToString(final[6:8]))
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-31 17:24:21 +09:00
|
|
|
var r = rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
|
|
|
|
2023-08-25 12:31:32 +09:00
|
|
|
func makeCouponCodes(name string, count int) (string, map[string]string) {
|
|
|
|
|
checkunique := make(map[string]bool)
|
|
|
|
|
keys := make(map[string]string)
|
|
|
|
|
uid := make([]byte, 4)
|
|
|
|
|
|
|
|
|
|
roundHash, roundnum := coupon.MakeCouponRoundHash(name)
|
|
|
|
|
|
|
|
|
|
for len(keys) < count {
|
2023-08-31 17:24:21 +09:00
|
|
|
r.Read(uid)
|
2023-08-25 12:31:32 +09:00
|
|
|
|
|
|
|
|
code := makeCouponKey(roundnum, uid)
|
|
|
|
|
|
|
|
|
|
if _, ok := checkunique[code]; !ok {
|
|
|
|
|
checkunique[code] = true
|
|
|
|
|
keys[hex.EncodeToString(uid)] = code
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return roundHash, keys
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func generateCoupons(mongoClient gocommon.MongoClient, w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
name, _ := gocommon.ReadStringFormValue(r.Form, "name")
|
|
|
|
|
effect, _ := gocommon.ReadStringFormValue(r.Form, "effect")
|
|
|
|
|
count, _ := gocommon.ReadIntegerFormValue(r.Form, "count")
|
|
|
|
|
desc, _ := gocommon.ReadStringFormValue(r.Form, "desc")
|
|
|
|
|
|
|
|
|
|
if count == 0 {
|
|
|
|
|
logger.Println("[generateCoupons] count == 0")
|
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
roundHash, _ := coupon.MakeCouponRoundHash(name)
|
|
|
|
|
roundObj, _ := primitive.ObjectIDFromHex(roundHash + roundHash + roundHash)
|
|
|
|
|
|
|
|
|
|
if count < 0 {
|
|
|
|
|
// 무한 쿠폰이므로 그냥 문서 생성해 주고 끝
|
|
|
|
|
if _, _, err := mongoClient.Update(CollectionCoupon, bson.M{
|
|
|
|
|
"_id": roundObj,
|
|
|
|
|
}, bson.M{
|
|
|
|
|
"$set": &couponDoc{
|
|
|
|
|
Name: name,
|
|
|
|
|
Effect: effect,
|
|
|
|
|
Desc: desc,
|
|
|
|
|
Total: -1,
|
|
|
|
|
},
|
|
|
|
|
}, options.Update().SetUpsert(true)); err != nil {
|
|
|
|
|
logger.Println("[generateCoupons] Update failed :", err)
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// effect가 비어있으면 기존의 roundName에 갯수를 추가해 준다
|
|
|
|
|
// effect가 비어있지 않으면 roundName이 겹쳐서는 안된다.
|
|
|
|
|
coupondoc, err := mongoClient.FindOne(CollectionCoupon, bson.M{"_id": roundObj})
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Println("[generateCoupons] FindOne failed :", err)
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
lastKeys := make(map[string]bool)
|
|
|
|
|
if coupondoc != nil {
|
|
|
|
|
if r, ok := coupondoc["remains"]; ok {
|
|
|
|
|
remains := r.(primitive.A)
|
|
|
|
|
for _, uid := range remains {
|
|
|
|
|
lastKeys[uid.(string)] = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
issuedKeys := make(map[string]string)
|
|
|
|
|
for len(issuedKeys) < int(count) {
|
|
|
|
|
_, vs := makeCouponCodes(name, int(count)-len(issuedKeys))
|
|
|
|
|
for k, v := range vs {
|
|
|
|
|
if _, ok := lastKeys[k]; !ok {
|
|
|
|
|
// 기존 키와 중복되지 않는 것만
|
|
|
|
|
issuedKeys[k] = v
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var coupons []string
|
|
|
|
|
var uids []string
|
|
|
|
|
for uid, code := range issuedKeys {
|
|
|
|
|
uids = append(uids, uid)
|
|
|
|
|
coupons = append(coupons, code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if coupondoc != nil {
|
|
|
|
|
_, _, err = mongoClient.Update(CollectionCoupon, bson.M{
|
|
|
|
|
"_id": roundObj,
|
|
|
|
|
}, bson.M{
|
|
|
|
|
"$push": bson.M{"remains": bson.M{"$each": uids}},
|
|
|
|
|
"$inc": bson.M{"total": count},
|
|
|
|
|
}, options.Update().SetUpsert(true))
|
|
|
|
|
} else {
|
|
|
|
|
_, _, err = mongoClient.Update(CollectionCoupon, bson.M{
|
|
|
|
|
"_id": roundObj,
|
|
|
|
|
}, bson.M{
|
|
|
|
|
"$push": bson.M{"remains": bson.M{"$each": uids}},
|
|
|
|
|
"$set": couponDoc{
|
|
|
|
|
Name: name,
|
|
|
|
|
Effect: effect,
|
|
|
|
|
Desc: desc,
|
|
|
|
|
Total: count,
|
|
|
|
|
},
|
|
|
|
|
}, options.Update().SetUpsert(true))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Println("[generateCoupons] Update failed :", err)
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enc := json.NewEncoder(w)
|
|
|
|
|
enc.Encode(coupons)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func downloadCoupons(mongoClient gocommon.MongoClient, w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
name, _ := gocommon.ReadStringFormValue(r.Form, "name")
|
|
|
|
|
if len(name) == 0 {
|
|
|
|
|
logger.Println("[downloadCoupons] name is empty")
|
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
round, _ := coupon.MakeCouponRoundHash(name)
|
|
|
|
|
|
|
|
|
|
roundObj, err := primitive.ObjectIDFromHex(round + round + round)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// 유효하지 않은 형식의 code
|
|
|
|
|
logger.Println("[downloadCoupons] ObjectIDFromHex failed :", err)
|
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var coupon couponDoc
|
|
|
|
|
if err := mongoClient.FindOneAs(CollectionCoupon, bson.M{
|
|
|
|
|
"_id": roundObj,
|
|
|
|
|
}, &coupon, options.FindOne().SetProjection(bson.M{"_id": 0, "remains": 1})); err != nil {
|
|
|
|
|
logger.Println("[downloadCoupons] FindOne failed :", err)
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
roundnum := binary.BigEndian.Uint32(roundObj[:])
|
|
|
|
|
var coupons []string
|
|
|
|
|
for _, uid := range coupon.Remains {
|
2024-02-26 15:51:49 +09:00
|
|
|
decUid, err := hex.DecodeString(uid)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Println("downloadCoupons Fail", err)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
coupons = append(coupons, makeCouponKey(roundnum, decUid))
|
2023-08-25 12:31:32 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enc := json.NewEncoder(w)
|
|
|
|
|
enc.Encode(coupons)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func queryCoupon(mongoClient gocommon.MongoClient, w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
code, _ := gocommon.ReadStringFormValue(r.Form, "code")
|
|
|
|
|
if len(code) == 0 {
|
|
|
|
|
logger.Println("[queryCoupon] code is empty")
|
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
round, _ := coupon.DisolveCouponCode(code)
|
|
|
|
|
if len(round) == 0 {
|
|
|
|
|
// 유효하지 않은 형식의 code
|
|
|
|
|
// 쿠폰 이름일 수 있으므로 round hash를 계산한다.
|
|
|
|
|
round, _ = coupon.MakeCouponRoundHash(code)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
roundObj, err := primitive.ObjectIDFromHex(round + round + round)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// 유효하지 않은 형식의 code
|
|
|
|
|
logger.Println("[queryCoupon] ObjectIDFromHex failed :", err)
|
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var coupon couponDoc
|
|
|
|
|
if err := mongoClient.FindOneAs(CollectionCoupon, bson.M{
|
|
|
|
|
"_id": roundObj,
|
2023-08-29 11:06:14 +09:00
|
|
|
}, &coupon, options.FindOne().SetProjection(bson.M{"effect": 1, "name": 1, "reason": 1, "total": 1, "desc": 1}).SetReturnKey(false)); err != nil {
|
2023-08-25 12:31:32 +09:00
|
|
|
logger.Println("[queryCoupon] FindOneAs failed :", err)
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enc := json.NewEncoder(w)
|
|
|
|
|
enc.Encode(coupon)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func listAllCouponNames(mongoClient gocommon.MongoClient, w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
all, err := mongoClient.FindAll(CollectionCoupon, bson.M{}, options.Find().SetProjection(bson.M{"name": 1}))
|
|
|
|
|
if err != nil {
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var names []string
|
|
|
|
|
for _, doc := range all {
|
|
|
|
|
names = append(names, doc["name"].(string))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
enc := json.NewEncoder(w)
|
|
|
|
|
enc.Encode(names)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func useCoupon(mongoClient gocommon.MongoClient, w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
acc, ok := gocommon.ReadObjectIDFormValue(r.Form, "accid")
|
|
|
|
|
if !ok || acc.IsZero() {
|
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
code, _ := gocommon.ReadStringFormValue(r.Form, "code")
|
|
|
|
|
code = strings.TrimSpace(code)
|
|
|
|
|
if len(code) == 0 {
|
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
round, key := coupon.DisolveCouponCode(code)
|
|
|
|
|
if len(round) == 0 {
|
|
|
|
|
// couponId가 쿠폰 이름일 수도 있다. 무한 쿠폰
|
|
|
|
|
round, _ = coupon.MakeCouponRoundHash(code)
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-26 15:51:49 +09:00
|
|
|
// 쿠폰 사용 유무 검사
|
|
|
|
|
alreadyused, err := mongoClient.Exists(CollectionCouponUse, bson.M{
|
2023-08-25 12:31:32 +09:00
|
|
|
"_id": acc,
|
|
|
|
|
"rounds": round,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Println(err)
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-26 15:51:49 +09:00
|
|
|
if alreadyused {
|
2023-08-25 12:31:32 +09:00
|
|
|
// 이미 이 라운드의 쿠폰을 사용한 적이 있다.
|
|
|
|
|
w.WriteHeader(http.StatusConflict)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var coupon couponDoc
|
|
|
|
|
roundObj, _ := primitive.ObjectIDFromHex(round + round + round)
|
|
|
|
|
if len(key) == 0 {
|
|
|
|
|
// 무한 쿠폰일 수 있으므로 존재하는지 확인
|
|
|
|
|
if err := mongoClient.FindOneAs(CollectionCoupon, bson.M{
|
|
|
|
|
"_id": roundObj,
|
|
|
|
|
}, &coupon, options.FindOne().SetProjection(bson.M{"_id": 0, "effect": 1, "name": 1, "reason": 1, "total": 1})); err != nil {
|
|
|
|
|
logger.Println(err)
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if coupon.Total > 0 {
|
|
|
|
|
// 무한 쿠폰 아니네?
|
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// 2. 쿠폰을 하나 꺼냄
|
|
|
|
|
matched, _, err := mongoClient.Update(CollectionCoupon, bson.M{
|
2024-02-26 15:51:49 +09:00
|
|
|
"_id": roundObj,
|
|
|
|
|
"remains": key,
|
2023-08-25 12:31:32 +09:00
|
|
|
}, bson.M{
|
|
|
|
|
"$pull": bson.M{"remains": key},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Println(err)
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !matched {
|
|
|
|
|
// 쿠폰이 없다.
|
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 3. round의 효과 읽기
|
|
|
|
|
if err := mongoClient.FindOneAndUpdateAs(CollectionCoupon, bson.M{
|
|
|
|
|
"_id": roundObj,
|
|
|
|
|
}, bson.M{
|
|
|
|
|
"$push": bson.M{"used": key},
|
|
|
|
|
}, &coupon, options.FindOneAndUpdate().SetProjection(bson.M{"effect": 1})); err != nil {
|
|
|
|
|
logger.Println(err)
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-26 15:51:49 +09:00
|
|
|
if coupon.Expire < time.Now().Unix() {
|
|
|
|
|
// 쿠폰 만료시간 경과
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-25 12:31:32 +09:00
|
|
|
if len(coupon.Effect) == 0 {
|
|
|
|
|
// 쿠폰이 없네?
|
|
|
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 4. 쿠폰은 사용한 것으로 표시
|
|
|
|
|
// 이제 이 아래에서 실패하면 이 쿠폰은 못쓴다.
|
|
|
|
|
updated, _, err := mongoClient.Update(CollectionCouponUse, bson.M{
|
|
|
|
|
"_id": acc,
|
|
|
|
|
}, bson.M{
|
|
|
|
|
"$push": bson.M{"rounds": round},
|
|
|
|
|
"$set": bson.M{round + ".id": code},
|
|
|
|
|
"$currentDate": bson.M{round + ".ts": true},
|
|
|
|
|
}, options.Update().SetUpsert(true))
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.Println(err)
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !updated {
|
|
|
|
|
logger.Println(err)
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
w.Write([]byte(coupon.Effect))
|
|
|
|
|
}
|