March 28, 2020
By: Kevin

团购的逻辑

基本情况

  • 历时两小时
  • 技术实现
  • clojure数据结构写入edn
  • 从edn恢复数据结构
  • java-time java.utils.Date的转化和时间计算
  • core.async模拟异步事件

拼团的基本流程

  • 用户发起团购
  • 其他用户可以加入团购
  • 用户团购有倒计时, 到时候凑不够人需要给所有的用户退款
  • 服务器中间重启的时候需要能够恢复现场

   +---------------+
   |   init group  |
   +-----+---------+
         |       +--------------+
         |<------+ people add.. |
         |       +--------------+
24 hours |       +--------------+
         |<------+ more added.. |
         |       +--------------+
         |       +--------------+
         |<------+ ..........   |
  +------v-------+-+------------+
  |    end group   |
  +----------------+


拼团的功能点

除了最后一部分功能均有实现

  - [100%] process
     - [X] 倒计时机制
     - [X] 组织db
       - [X] 记录每个拼团活动的状态
       - [X] 记录倒计时任务中关参加的用户
     - [X] 持久化存储
       - [X] 读取db
       - [X] 写入db
     - [X] 子任务
       - [X] 倒计时开始
         - [X] 时间计算
       - [X] 加入新的拼团
         - [X] 更新每个团的用户情况
       - [X] 拼团结束
         - [X] 凑够人数了, 拼团成功
         - [X] 没有凑够人数, 拼团失败, 需要给用户退款
       - [X] 服务器重启后的定时任务恢复
         - [X] 要求我们前面的步骤有磁盘存储(或者数据库)
         - [X] 还没到时间的拼团我们恢复进行
       - 服务器重启后过期的定时任务的清理
         - [X] 过期的拼团
           - [X] 成功凑够了人数: 修改状态为拼团成功
           - [X] 没有凑够人数:需要给用户退款
         - [X] 失败的退款
           - [-] 退款没有收到回掉的情况,需要和服务器确认退款情况

拼团的逻辑实现

(require '[chime :refer [chime-ch]]
         '[java-time :as jt]
         '[clojure.core.async :as a :refer [<! go-loop]])


(def db (atom {}))

(add-watch db :watcher
           (fn [key atom old-state new-state]
             (prn "-- Atom Changed --")
             (prn "old-state")
             (clojure.pprint/pprint old-state)
             (prn "new-state")
             (clojure.pprint/pprint new-state)))

(defn save-to-db []
  (spit "group.edn" @db))

(defn recover-from-db []
  (reset! db (clojure.edn/read-string (slurp "group.edn" ))))

(str (java.util.UUID/randomUUID))
;; => "fb16dd4c-1284-467f-be6f-7982d5fa9971"

(defn init-group [group-id user-id end-time]
  (swap! db assoc group-id {:end-time (java.util.Date/from end-time)
                            :starter user-id
                            :status :拼团中
                            :price 100
                            :members {user-id {:pay-status :paied}}})
  (save-to-db))

(defn- group-open? [group-id]
  (if (= :拼团中 (get-in @db [group-id :status]) )
    true
    false))

(defn join-group [group-id user-id]
  (when (group-open? group-id)
    (swap! db assoc-in [group-id :members user-id]  {:pay-status :paied})
    (save-to-db)))

(defn group-full? [group-id]
  (if (> (count (get-in @db [group-id :members])) 2)
    true
    false))

(defn user-refund [group-id user-id]
  (a/go
    (prn "开始给用户" user-id "参加的团" group-id "退款开始")
    (swap! db assoc-in [group-id :members user-id :pay-status] :refunding)
    (save-to-db)
    (<! (a/timeout (rand-int 2000)))
    (prn "开始给用户" user-id "参加的团" group-id "退款成功")
    (swap! db assoc-in [group-id :members user-id :pay-status] :refunded)
    (save-to-db)))

(defn group-refund [group-id]
  (doseq [user-id (keys (get-in @db [group-id :members]))]
    (user-refund group-id user-id)))

(defn group-failed-for-not-enough-people [group-id]
  (group-refund group-id)
  (swap! db assoc-in [group-id :status ] :拼团没凑够人)
  (save-to-db))

(defn group-end-handling [group-id]
  (if (group-full? group-id)
    (do (swap! db assoc-in [group-id :status ] :拼团成功结束)
        (save-to-db))
    (group-failed-for-not-enough-people group-id)
    ))

(defn timer [group-id end-time]
  (let [chimes (chime-ch [end-time])]
    (go
      (when-let [msg (<! chimes)]
        (prn "拼团" group-id "结束")
        (group-end-handling group-id)))))

(defn start-a-group-activity [user-id group-id end-time]
  (init-group group-id user-id end-time)
  (timer group-id end-time))


(defn future? [date]
  (jt/after? (.toInstant date)  (jt/instant)))

(defn past? [date]
  (not (future? date)))

(defn missed-groups []
  (reduce-kv (fn [m k v]
               (when (and (past? (:end-time v) )
                          (= :拼团中 (:status v)))
                 (assoc m k v)))
             {}
             @db))

(defn handle-missed-group [group]
  (let [group-id (first (keys group))]
    (if (group-full? group-id)
      (timer group-id (jt/plus (jt/instant) (jt/seconds 1)))
      (group-refund group-id))))

(defn refund-for-missed-groups []
  (let [groups-need-refund (missed-groups)]
    (reduce-kv (fn [m k v]
                 (handle-missed-group k))
               {}
               groups-need-refund)))

(defn normal-groups []
  (reduce-kv (fn [m k v]
               (when (future? (:end-time v) )
                 (assoc m k v)))
             {}
             @db))

(defn resume-normal-groups []
  (->> (normal-groups)
       (reduce-kv
        (fn [m k v]
          (timer k (.toInstant (:end-time v))))
        {})
       ))

(defn recover-all []
  (recover-from-db)
  (refund-for-missed-groups)
  (resume-normal-groups))

(defn resume-groups []
  (let [current (jt/instant)]))

(defn two [] (jt/plus (jt/instant) (jt/seconds 2)))
(defn ten [] (jt/plus (jt/instant) (jt/seconds 20)))

(defn one-m [] (jt/plus (jt/instant) (jt/minutes 2)))



(comment
  @db
  (reset! db {})
  (save-to-db)
  (recover-from-db)
  (timer "fb16dd4c-1284-467f-be6f-7982d5fa9971" two)
  (init-group "fb16dd4c-1284-467f-be6f-7982d5fa9971" 1 two)
  (start-a-group-activity 1 "fb16dd4c-1284-467f-be6f-7982d5fa9971" (one-m))

  (start-a-group-activity 2 "fb16dd4c-1284-467f-be6f-7982d5fa9972" (two))
  (join-group "fb16dd4c-1284-467f-be6f-7982d5fa9971" 2 )
  (join-group "fb16dd4c-1284-467f-be6f-7982d5fa9971" 3 )

  (resume-normal-groups)
  (recover-all)
  )

Tags: clojure