266 lines
8.8 KiB
Go
266 lines
8.8 KiB
Go
package handler
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"past/internal/httpapi"
|
|
"past/internal/store"
|
|
)
|
|
|
|
func guardSmokingCheckin(c *httpapi.Ctx, id int64) error {
|
|
row, err := c.DB.QueryOne("SELECT id FROM smoking_accounts WHERE checkin_id = ?", id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if row != nil {
|
|
return c.Err(400, "请通过抽烟记录操作库存和撤销")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func smokingDelta(kind string, quantity, stock int64) (int64, error) {
|
|
if quantity < 0 || quantity > 1000000 {
|
|
return 0, errors.New("数量超出范围")
|
|
}
|
|
switch kind {
|
|
case "consume", "give":
|
|
if quantity != 1 {
|
|
return 0, errors.New("每次操作一支")
|
|
}
|
|
if stock < 1 {
|
|
return 0, errors.New("库存不足,请先补充库存")
|
|
}
|
|
return -1, nil
|
|
case "receive_smoke":
|
|
if quantity != 1 {
|
|
return 0, errors.New("每次记录一支")
|
|
}
|
|
return 0, nil
|
|
case "purchase", "receive":
|
|
if quantity < 1 || quantity > 10000 {
|
|
return 0, errors.New("补充数量应为1到10000支")
|
|
}
|
|
return quantity, nil
|
|
case "adjust":
|
|
return quantity - stock, nil
|
|
default:
|
|
return 0, errors.New("不支持的操作")
|
|
}
|
|
}
|
|
|
|
func smokingSnapshot(c *httpapi.Ctx, uid int64) (M, error) {
|
|
result := M{"stock": int64(0), "today_count": int64(0), "total_count": int64(0), "week_count": int64(0), "month_count": int64(0), "week_purchase_cents": int64(0), "month_purchase_cents": int64(0), "logs": []any{}, "daily": []any{}, "last_time": "", "user_id": uid, "read_only": uid != c.UID(), "available": true}
|
|
owner, err := c.DB.QueryOne("SELECT nickname, username FROM users WHERE id = ?", uid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if owner != nil {
|
|
result["owner_name"] = owner.Str("nickname")
|
|
if owner.Str("nickname") == "" {
|
|
result["owner_name"] = owner.Str("username")
|
|
}
|
|
}
|
|
partner := partnerUserId(c.DB, c.UID())
|
|
result["partner_id"] = partner
|
|
account, err := c.DB.QueryOne("SELECT user_id AS id, stock FROM smoking_wallets WHERE user_id = ?", uid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if account == nil {
|
|
return result, nil
|
|
}
|
|
aid := account.Int("id")
|
|
result["stock"] = account.Int("stock")
|
|
summary, err := c.DB.QueryOne(`SELECT
|
|
COALESCE(SUM(kind IN ('consume','receive_smoke')),0) AS total_count,
|
|
COALESCE(SUM(kind IN ('consume','receive_smoke') AND created_at >= CURRENT_DATE),0) AS today_count,
|
|
COALESCE(SUM(kind IN ('consume','receive_smoke') AND created_at >= ?),0) AS week_count,
|
|
COALESCE(SUM(kind IN ('consume','receive_smoke') AND created_at >= ?),0) AS month_count,
|
|
COALESCE(SUM(CASE WHEN kind = 'purchase' AND created_at >= ? THEN amount_cents ELSE 0 END),0) AS week_purchase_cents,
|
|
COALESCE(SUM(CASE WHEN kind = 'purchase' AND created_at >= ? THEN amount_cents ELSE 0 END),0) AS month_purchase_cents,
|
|
MAX(CASE WHEN kind IN ('consume','receive_smoke') THEN created_at ELSE NULL END) AS last_time
|
|
FROM smoking_entries WHERE user_id = ? AND undone = 0`, weekMonday(), monthStart(), weekMonday(), monthStart(), aid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if summary != nil {
|
|
result["today_count"] = summary.Int("today_count")
|
|
result["total_count"] = summary.Int("total_count")
|
|
for _, field := range []string{"week_count", "month_count", "week_purchase_cents", "month_purchase_cents"} {
|
|
result[field] = summary.Int(field)
|
|
}
|
|
result["last_time"] = summary.Str("last_time")
|
|
}
|
|
logs, err := c.DB.Query("SELECT id, kind, quantity, delta, balance, amount_cents, note, undone, created_at FROM smoking_entries WHERE user_id = ? ORDER BY id DESC LIMIT 50", aid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result["logs"] = rows(logs)
|
|
daily, err := c.DB.Query("SELECT DATE_FORMAT(created_at,'%Y-%m-%d') AS day, COUNT(*) AS count FROM smoking_entries WHERE user_id = ? AND kind IN ('consume','receive_smoke') AND undone = 0 AND created_at >= DATE_SUB(CURRENT_DATE, INTERVAL 6 DAY) GROUP BY DATE_FORMAT(created_at,'%Y-%m-%d') ORDER BY day", aid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result["daily"] = rows(daily)
|
|
return result, nil
|
|
}
|
|
|
|
func smokingInfo(c *httpapi.Ctx) error {
|
|
uid := c.UID()
|
|
requested := c.QInt("user_id")
|
|
if requested == 0 {
|
|
requested = uid
|
|
}
|
|
if requested != uid && requested != partnerUserId(c.DB, uid) {
|
|
return c.Err(403, "只能查看自己和当前伴侣的记录")
|
|
}
|
|
info, err := smokingSnapshot(c, requested)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.OK(info)
|
|
}
|
|
|
|
func smokingAction(c *httpapi.Ctx) error {
|
|
if err := c.RequirePOST(); err != nil {
|
|
return err
|
|
}
|
|
var err error
|
|
uid := c.UID()
|
|
kind, key, note := c.InTrim("kind"), c.InTrim("request_key"), c.InTrim("note")
|
|
quantity, quantityErr := strconv.ParseInt(c.InTrim("quantity"), 10, 64)
|
|
optionalInt := func(key string) (int64, error) {
|
|
value := c.InTrim(key)
|
|
if value == "" {
|
|
return 0, nil
|
|
}
|
|
return strconv.ParseInt(value, 10, 64)
|
|
}
|
|
amount, amountErr := optionalInt("amount_cents")
|
|
originalID, originalErr := optionalInt("original_id")
|
|
if quantityErr != nil || amountErr != nil || originalErr != nil || (kind != "purchase" && amount != 0) || (kind != "undo" && originalID != 0) || (kind == "undo" && (originalID <= 0 || quantity != 0)) {
|
|
return c.Err(400, "数量、金额和记录标识必须是有效整数")
|
|
}
|
|
if key == "" || len(key) > 64 || utf8.RuneCountInString(note) > 200 || amount < 0 || amount > 100000000 {
|
|
return c.Err(400, "参数不正确")
|
|
}
|
|
if kind != "undo" {
|
|
if _, err := smokingDelta(kind, quantity, 1000000); err != nil {
|
|
return c.Err(400, err.Error())
|
|
}
|
|
}
|
|
if note != "" {
|
|
if err := checkTextOr400(c, note); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
payload, _ := json.Marshal([]any{kind, quantity, amount, originalID, note})
|
|
hash := fmt.Sprintf("%x", sha256.Sum256(payload))
|
|
tx, err := c.DB.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
if _, err = tx.Exec("INSERT IGNORE INTO smoking_wallets (user_id) VALUES (?)", uid); err != nil {
|
|
return err
|
|
}
|
|
account, err := tx.QueryOne("SELECT user_id AS id,stock FROM smoking_wallets WHERE user_id = ? FOR UPDATE", uid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if account == nil {
|
|
return errors.New("库存账户创建失败")
|
|
}
|
|
aid, stock := account.Int("id"), account.Int("stock")
|
|
previous, err := tx.QueryOne("SELECT payload_hash FROM smoking_entries WHERE user_id = ? AND request_key = ?", aid, key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if previous != nil {
|
|
if previous.Str("payload_hash") != hash {
|
|
return c.Err(409, "请求标识已被其他操作使用")
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
info, err := smokingSnapshot(c, uid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return c.OK(M{"success": true, "info": info})
|
|
}
|
|
var delta int64
|
|
if kind == "undo" {
|
|
original, err := tx.QueryOne("SELECT id,delta,kind,undone FROM smoking_entries WHERE user_id = ? AND id = ? FOR UPDATE", aid, originalID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if original == nil || original.Int("undone") != 0 || original.Str("kind") == "undo" {
|
|
return c.Err(409, "这条记录无法撤销或已经撤销")
|
|
}
|
|
delta = -original.Int("delta")
|
|
quantity = 0
|
|
if stock+delta < 0 {
|
|
return c.Err(409, "库存已使用,无法撤销这次补充,请通过调整库存修正")
|
|
}
|
|
if _, err = tx.Exec("UPDATE smoking_entries SET undone = 1 WHERE id = ?", originalID); err != nil {
|
|
return err
|
|
}
|
|
|
|
} else {
|
|
delta, err = smokingDelta(kind, quantity, stock)
|
|
if err != nil {
|
|
return c.Err(400, err.Error())
|
|
}
|
|
}
|
|
if stock+delta > 1000000 && delta > 0 {
|
|
return c.Err(400, "库存不能超过100万支")
|
|
}
|
|
if _, err = tx.Exec("UPDATE smoking_wallets SET stock = ? WHERE user_id = ?", stock+delta, aid); err != nil {
|
|
return err
|
|
}
|
|
if _, err = tx.Insert("INSERT INTO smoking_entries (user_id,kind,quantity,delta,balance,amount_cents,note,request_key,payload_hash,original_id) VALUES (?,?,?,?,?,?,?,?,?,?)", aid, kind, quantity, delta, stock+delta, amount, strings.TrimSpace(note), key, hash, nullableID(originalID)); err != nil {
|
|
return err
|
|
}
|
|
if err = tx.Commit(); err != nil {
|
|
return err
|
|
}
|
|
info, err := smokingSnapshot(c, uid)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// 仅记录抽烟/消耗类动作时通知伴侣(撤销、补库存不打扰)
|
|
var smokeDesc string
|
|
switch kind {
|
|
case "consume":
|
|
smokeDesc = "TA 记录抽了一支烟"
|
|
case "give":
|
|
smokeDesc = "TA 递出了一支烟"
|
|
case "receive_smoke":
|
|
smokeDesc = "TA 记录收到一支烟"
|
|
}
|
|
if smokeDesc != "" {
|
|
if partnerID := partnerUserId(c.DB, uid); partnerID != 0 {
|
|
actID := logActivity(c.DB, c.CoupleID(), uid, "smoking_consume", smokeDesc, "")
|
|
c.App.Wechat.SendSubscribeMessage(partnerID, "notify", map[string]string{
|
|
"thing1": "戒烟打卡",
|
|
"thing2": fmt.Sprintf("%s,今日已 %d 支", smokeDesc, store.ToInt(info["today_count"])),
|
|
"date3": today(),
|
|
"thing4": "点击查看详情",
|
|
}, "pages/punch/punch", actID, uid)
|
|
}
|
|
}
|
|
return c.OK(M{"success": true, "info": info})
|
|
}
|
|
func nullableID(id int64) any {
|
|
if id == 0 {
|
|
return nil
|
|
}
|
|
return id
|
|
}
|