Files
past-go/past-go/internal/handler/smoking.go
T

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
}