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])) } var r = rand.New(rand.NewSource(time.Now().UnixNano())) 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 { r.Read(uid) 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 { decUid, err := hex.DecodeString(uid) if err != nil { logger.Println("downloadCoupons Fail", err) continue } coupons = append(coupons, makeCouponKey(roundnum, decUid)) } 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, }, &coupon, options.FindOne().SetProjection(bson.M{"effect": 1, "name": 1, "reason": 1, "total": 1, "desc": 1}).SetReturnKey(false)); err != nil { 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) } // 쿠폰 사용 유무 검사 alreadyused, err := mongoClient.Exists(CollectionCouponUse, bson.M{ "_id": acc, "rounds": round, }) if err != nil { logger.Println(err) w.WriteHeader(http.StatusInternalServerError) return } if alreadyused { // 이미 이 라운드의 쿠폰을 사용한 적이 있다. 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{ "_id": roundObj, "remains": key, }, 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 } } 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)) }