doushaju4901 2014-12-10 21:01
浏览 47
已采纳

付费会员处理

I have two tables, one is "transactions" table and one is "Membership " table.

A user can have all available membership active at once (each membership gives different benefits).

I'm trying to handle multiply transaction records with the same user id and the same Membership.

Here's my tables

Transactions table

ID | USERID | MID |      CREATED      | AMOUNT
-------------------------------------------------
 1  |   1   |  2  | 2014-10-01 00:00:00 |   1
 2  |   1   |  2  | 2014-10-16 00:00:00 |   1
 3  |   2   |  1  | 2014-10-30 00:00:00 |   1

Membership tables

ID |    TITLE    |  DURATION
-------------------------
 1 |   Premium   |     365
 2 |  Supporter  |     30
 3 | Beta Access |     30

In the transactions table I have 2 records for the userid 1, one that begins at 2014-10-01 and one at 2014-10-16.

The following script works fine for selecting individual active memberships logs

SELECT t.USERID AS UID, t.CREATED AS CREATED, FROM_UNIXTIME(UNIX_TIMESTAMP(t.CREATED) + t.AMOUNT * m.DURATION) AS ENDS
FROM Transactions AS t
LEFT JOIN Memberships AS m on m.ID = t.MID
LIMIT 5

Output would be this,

UID | MID |       CREATED      | ENDS
-----------------------------------------------------
 1  | 2  | 2014-10-01 00:00:00 | 2014-10-31 00:00:00
 1  | 2  | 2014-10-16 00:00:00 | 2014-11-15 00:00:00
 2  | 1  | 2014-10-30 00:00:00 | 2015-10-30 00:00:00

Now there is two records with the same Membership ID (MID) and user ID (UID), and the first record does expire before the second one.

Basically, what I'm trying to do, 'merge' or combine' the total 'unused' amount of days of a memberishp as long as another membership (same userid and same memebrship id) was added before the current has expired.

(Here's an example to display the data given and the wanted output:)

ID | USERID | MID |      CREATED        | Amount
-------------------------------------------------
 1  |   1   |  2  | 2014-10-01 00:00:00 |   1 #30 days days remains
 2  |   1   |  2  | 2014-10-17 00:00:00 |   1 #14 days of the previous transaction is not fully consumed,43 days remains - (days amount +  previous unused days)
 3  |   1   |  2  | 2014-11-01 00:00:00 |   1 #28 days of the previous transaction (44 days ones) is not fully consumed,59 days remains - (days amount +  previous unused days)
 4  |   2   |  3  | 2014-10-01 00:00:00 |   1 #30 days days remains
 5  |   2   |  3  | 2014-11-08 00:00:00 |   1 #30 days days remains

And output should be this

UID | MID |       CREATED       | ENDS
-----------------------------------------------------
 1  | 2  | 2015-10-01 00:00:00 | 2014-12-29 00:00:00
 2  | 1  | 2014-10-01 00:00:00 | 2014-10-30 00:00:00
 2  | 1  | 2014-11-08 00:00:00 | 2014-12-08 00:00:00

I apologize if not being clear, as English is not my native language, and do not have the words to explain what I'm trying to accomplish.

EDIT: Looking for php solution if not possible via mysql.

  • 写回答

1条回答 默认 最新

  • dsdsds12222 2014-12-18 16:35
    关注

    I'd say that there are mainly three different approaches and I try to give examples for those. However, all approaches have advantages/disadvantages too...

    MySQL query

    It seems that such a query requires some sort of recursion... So a potential approach can be similar to this one, and exploit the expression evaluation of MySQL, by using User-Defined Variables in the select statement.

    A query that returns the duration and the (extended) end of each transaction record:

    SELECT id, userid, mid, created,
      -- initialize if new userid or membership id
      IF (@lastuser!=userid or @lastmb!=mid, (@prevend:=created)+(@lastuser:=userid)+(@lastmb:=mid), 0) AS tmp,
      -- calculate unused days
      @unused:=IF(@prevend>created, datediff(@prevend, created), 0) AS tmp2, 
      -- calculate the end of current membership (will be used for next record)
      @prevend:=DATE_ADD(created, INTERVAL (amount * duration)+@unused DAY) AS ends,
      -- calculate the days remaining
      @unused+duration AS 'days remain'
    FROM (
      SELECT tt.id, tt.userid, tt.mid, tt.created, tt.amount, duration
      FROM transactions tt
      LEFT JOIN memberships as m on m.ID = tt.MID
      ORDER BY tt.userid, tt.created) t
    JOIN (SELECT @lastuser:=0)tmp;
    

    The output of this query is:

    id  userid mid  created             tmp     tmp2 ends                   days remain
    1   1      2    2014-10-01 00:00:00 2017    0    2014-10-31 00:00:00    30
    2   1      2    2014-10-17 00:00:00 0       14   2014-11-30 00:00:00    44
    3   1      2    2014-11-01 00:00:00 0       29   2014-12-30 00:00:00    59
    4   2      3    2014-10-01 00:00:00 2019    0    2014-10-31 00:00:00    30
    5   2      3    2014-11-08 00:00:00 0       0    2014-12-08 00:00:00    30
    

    It is still another task, to output just the merged intervals:

    SELECT userid, mid, begins, max(ends) as ends FROM (
        SELECT id, userid, mid, created,
          -- initialize if new userid or membership id
          IF (@lastuser!=userid or @lastmb!=mid, (@prevend:=created)+(@lastuser:=userid)+(@lastmb:=mid), 0) AS tmp,
          -- firstcreated stores the very first creation time of overlapping memberships 
          if (@prevend>created, @firstcreated, (@firstcreated:=created)) as begins,
          -- calculate unused days
          @unused:=IF(@prevend>created, datediff(@prevend, created), 0) AS tmp2, 
          -- calculate the end of current membership (will be used for next record)
          @prevend:=DATE_ADD(created, INTERVAL (amount * duration)+@unused DAY) AS ends,
          -- calculate the days remaining
          @unused+duration AS 'days remain'
        FROM (
          SELECT tt.id, tt.userid, tt.mid, tt.created, tt.amount, duration
          FROM transactions tt
          LEFT JOIN memberships as m on m.ID = tt.MID
          ORDER BY tt.userid, tt.created) t
        JOIN (SELECT @lastuser:=0)tmp
    ) mship 
    GROUP BY userid, mid, begins;
    

    Note, however, that this is really not an advisable solution, because the evaluation order of the expressions is not guaranteed. So the query might produce a good result, but with different data set, or with a different MySQL release, it may easily produce a bad result. In the proposed query, there is a subquery with an order by clause, so record order should not become a problem here, but if you want to put this query in a code that you wish to maintain longer, you might get surprised when you migrate to a new version of MySQL, for instance.

    At least, it seems to be working on MySQL 5.5 and MySQL 5.6 too.

    So caution again, because as the MySQL documentation says:

    As a general rule, other than in SET statements, you should never assign a value to a user variable and read the value within the same statement. For example, to increment a variable, this is okay:

    SET @a = @a + 1; For other statements, such as SELECT, you might get the results you expect, but this is not guaranteed. In the following statement, you might think that MySQL will evaluate @a first and then do an assignment second:

    SELECT @a, @a:=@a+1, ...;

    However, the order of evaluation for expressions involving user variables is undefined.

    Calculate everything on client (application) side (e.g. in PHP)

    The idea is the same. Fetch transactions ordered by userid, mid and creation date. Iterate over the records and for each new transaction extend the duration of the membership with the 'unused' dates (if there are any), which can be calculated from the previous transaction. When we see that there is a break in the membership, we save the actual period.

    A sample PHP code to do that:

    <?php
    $conn = mysqli_connect("localhost", "user", "password", "db");
    if (mysqli_connect_errno($conn)) {
        echo "Failed to connect to MySQL: " . mysqli_connect_error();
    }
    
    // Query to select membership information
    $res = mysqli_query($conn, "select t.id, userid, mid, created, (m.duration * t.amount) as duration
        from transactions t
        left join memberships m
        on t.mid=m.id
        order by userid, mid, created");
    
    echo "Temporary calculation:<br/>";
    echo "<table border=\"1\">";
    echo "<th>Id</th><th>UserId</th><th>MID</th><th>Created</th><th>Unused</th><th>End</th><th>Duration</th>";
    
    $last_userid=0;
    while ($row = $res->fetch_assoc()) {
        // Beginning of a new userid or membership id
        if ($row['userid']!=$last_userid or $row['mid']!=$last_mid) {
            // If we are not at the first record, we save the current period
            if ($last_userid!=0) {
                $mships[$last_userid][$last_mid][$first_created->format('Y-m-d H:i:s')]=$last_end->format('Y-m-d H:i:s');
            }
    
            // Initialize temporaries
            $last_userid=$row['userid'];
            $last_mid=$row['mid'];
            $first_created=new DateTime($row['created']);
            $last_end=clone $first_created;
        }
    
        // Calculate duration
        $created=new DateTime($row['created']);
        $unused=date_diff($created, $last_end);
        $ends=clone $created;
        $ends->add(new DateInterval("P".$row['duration']."D"));
    
        // $unused->invert is 1 if diff is negative 
        if ($unused->invert==0 && $unused->days>=0) {
            // This transaction extends/immediately follows the previous period
            $ends->add(new DateInterval('P'.$unused->days.'D'));
        } else {
            // We split the period -> save it!
            $mships[$row['userid']][$row['mid']][$first_created->format('Y-m-d H:i:s')]=$last_end->format('Y-m-d H:i:s');
            $first_created=new DateTime($row['created']);
        }
    
        $duration=date_diff($ends, $created);   
    
        echo "<tr>";
        echo "<td>",$row['id'],"</td>";
        echo "<td>",$row['userid'],"</td>";
        echo "<td>",$row['mid'],"</td>";
        echo "<td>",$row['created'],"</td>";
        echo "<td>",($unused->invert==0 ? $unused->format('%a') : 0),"</td>";
        echo "<td>",$ends->format('Y-m-d H:i:s'),"</td>";
        echo "<td>",$duration->format('%a'),"</td>";
        echo "</tr>";
    
        $last_end=$ends;
    }
    // Last period should be saved
    if ($last_userid!=0) {
        $mships[$last_userid][$last_mid][$first_created->format('Y-m-d H:i:s')]=$last_end->format('Y-m-d H:i:s');
    }
    
    echo "</table><br/>";
    
    echo "Final array:<br/>";
    echo "<table border=\"1\">";
    echo "<th>UserId</th><th>MID</th><th>Created</th><th>End</th>";
    
    foreach ($mships as $uid => &$mids) {
        foreach ($mids as $mid => &$periods) {
            foreach ($periods as $begin => $end) {
                echo "<tr>";
                echo "<td>",$uid,"</td>";
                echo "<td>",$mid,"</td>";
                echo "<td>",$begin,"</td>";
                echo "<td>",$end,"</td>";
                echo "</tr>";               
            }
        }
    }
    
    $conn->close();
    
    ?>
    

    (Honestly, it has been a few years since I last wrote anything in php :) So feel free to reformat or use some nicer solution.)

    Output should look like:

    Temporary calculation:
    Id  UserId  MID Created             Unused  End                 Duration
    1   1       2   2014-10-01 00:00:00 0       2014-10-31 00:00:00 30
    2   1       2   2014-10-17 00:00:00 14      2014-11-30 00:00:00 44
    3   1       2   2014-11-01 00:00:00 29      2014-12-30 00:00:00 59
    4   2       3   2014-10-01 00:00:00 0       2014-10-31 00:00:00 30
    5   2       3   2014-11-08 00:00:00 0       2014-12-08 00:00:00 30
    
    Final results:
    UserId  MID Created             End
    1       2   2014-10-01 00:00:00 2014-12-30 00:00:00
    2       3   2014-10-01 00:00:00 2014-10-31 00:00:00
    2       3   2014-11-08 00:00:00 2014-12-08 00:00:00
    

    MySQL stored procedure

    It is also possible to calculate the result set with a stored procedure. E.g. a sample procedure with the same algorithm as the previous PHP code.

    DROP PROCEDURE IF EXISTS get_memberships;
    
    delimiter //
    
    CREATE PROCEDURE get_memberships()
    BEGIN
      DECLARE done INT DEFAULT FALSE;
      DECLARE uid, mid, duration INT;
      DECLARE created, unused, first_created, ends TIMESTAMP;
      -- make sure that there is no user with 0 id
      DECLARE last_uid, last_mid INT DEFAULT 0;
      DECLARE last_end TIMESTAMP;
      DECLARE cur CURSOR FOR SELECT t.userid, t.mid, t.created, (m.duration * t.amount) as duration
        FROM transactions t
        LEFT JOIN memberships m
        ON t.mid=m.id
        ORDER BY userid, mid, created;
      DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE;
    
      OPEN cur;
    
      REPEAT
        FETCH cur INTO uid, mid, created, duration;
        IF (!done) THEN
            IF (uid!=last_uid OR last_mid!=mid) THEN
                IF (last_uid!=0) THEN
                    INSERT INTO results (userid, mid, created, ends) VALUES (last_uid, last_mid, first_created, last_end);
                END IF;
    
                SET last_uid = uid;
                SET last_mid = mid;
                SET last_end = created;
                SET first_created = created;
            END IF;
    
            SET ends = DATE_ADD(created, INTERVAL duration DAY);
    
            IF (last_end>=created) THEN
                SET ends = DATE_ADD(ends, INTERVAL datediff(last_end, created) DAY);
            ELSE
                INSERT INTO results (userid, mid, created, ends) VALUES (uid, mid, first_created, last_end);
                SET first_created = created;
            END IF;
    
            SET last_end = ends;
        END IF;
      UNTIL done
      END REPEAT;
    
      IF (last_uid!=0) THEN
        INSERT INTO results (userid, mid, created, ends) VALUES (uid, last_mid, first_created, last_end);
      END IF;
    
      CLOSE cur;
    END
    //
    
    DROP TABLE IF EXISTS results //
    CREATE TEMPORARY TABLE results AS SELECT userid, mid, created, created as ends FROM transactions WHERE 0 //
    call get_memberships //
    SELECT * FROM results //
    DROP TABLE results;
    

    However, one disadvantage of this technique is the usage of a temporary table.

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

悬赏问题

  • ¥15 c程序不知道为什么得不到结果
  • ¥40 复杂的限制性的商函数处理
  • ¥15 程序不包含适用于入口点的静态Main方法
  • ¥15 素材场景中光线烘焙后灯光失效
  • ¥15 请教一下各位,为什么我这个没有实现模拟点击
  • ¥15 执行 virtuoso 命令后,界面没有,cadence 启动不起来
  • ¥50 comfyui下连接animatediff节点生成视频质量非常差的原因
  • ¥20 有关区间dp的问题求解
  • ¥15 多电路系统共用电源的串扰问题
  • ¥15 slam rangenet++配置