增加抽烟周月支数及买烟金额统计
This commit is contained in:
@@ -55,7 +55,7 @@ func smokingDelta(kind string, quantity, stock int64) (int64, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func smokingSnapshot(c *httpapi.Ctx, uid int64) (M, error) {
|
func smokingSnapshot(c *httpapi.Ctx, uid int64) (M, error) {
|
||||||
result := M{"stock": int64(0), "today_count": int64(0), "total_count": int64(0), "logs": []any{}, "daily": []any{}, "last_time": "", "user_id": uid, "read_only": uid != c.UID(), "available": true}
|
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)
|
owner, err := c.DB.QueryOne("SELECT nickname, username FROM users WHERE id = ?", uid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -77,13 +77,24 @@ func smokingSnapshot(c *httpapi.Ctx, uid int64) (M, error) {
|
|||||||
}
|
}
|
||||||
aid := account.Int("id")
|
aid := account.Int("id")
|
||||||
result["stock"] = account.Int("stock")
|
result["stock"] = account.Int("stock")
|
||||||
summary, err := c.DB.QueryOne("SELECT COUNT(*) AS total_count, COALESCE(SUM(created_at >= CURRENT_DATE),0) AS today_count, MAX(created_at) AS last_time FROM smoking_entries WHERE user_id = ? AND kind IN ('consume','receive_smoke') AND undone = 0", aid)
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if summary != nil {
|
if summary != nil {
|
||||||
result["today_count"] = summary.Int("today_count")
|
result["today_count"] = summary.Int("today_count")
|
||||||
result["total_count"] = summary.Int("total_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")
|
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)
|
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)
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package handler
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
"past/internal/testdb"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSmokingSnapshotWeeklyMonthlyStats(t *testing.T) {
|
||||||
|
for _, wallet := range []bool{false, true} {
|
||||||
|
t.Run(fmt.Sprint(wallet), func(t *testing.T) {
|
||||||
|
account := [][]driver.Value(nil)
|
||||||
|
if wallet {
|
||||||
|
account = [][]driver.Value{{int64(2), int64(18)}}
|
||||||
|
}
|
||||||
|
steps := []testdb.Step{
|
||||||
|
uidStep(1),
|
||||||
|
{Match: "SELECT nickname, username", Columns: []string{"nickname", "username"}, Rows: [][]driver.Value{{"TA", "partner"}}},
|
||||||
|
{Match: "SELECT couple_id FROM users", Columns: []string{"couple_id"}, Rows: [][]driver.Value{{int64(10)}}},
|
||||||
|
{Match: "SELECT user1_id, user2_id FROM couples", Columns: []string{"user1_id", "user2_id"}, Rows: [][]driver.Value{{int64(1), int64(2)}}},
|
||||||
|
{Match: "FROM smoking_wallets", Args: []any{int64(2)}, Columns: []string{"id", "stock"}, Rows: account},
|
||||||
|
}
|
||||||
|
if wallet {
|
||||||
|
steps = append(steps,
|
||||||
|
testdb.Step{Match: "AS week_purchase_cents", Args: []any{weekMonday(), monthStart(), weekMonday(), monthStart(), int64(2)}, Check: func(query string, args []any) error {
|
||||||
|
for _, expected := range []string{"kind IN ('consume','receive_smoke')", "kind = 'purchase'", "AND undone = 0", "AS month_purchase_cents"} {
|
||||||
|
if !strings.Contains(query, expected) {
|
||||||
|
return fmt.Errorf("missing %s", expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(query, "LIMIT") {
|
||||||
|
return fmt.Errorf("statistics must cover all entries")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}, Columns: []string{"today_count", "total_count", "week_count", "month_count", "week_purchase_cents", "month_purchase_cents", "last_time"}, Rows: [][]driver.Value{{int64(1), int64(120), int64(7), int64(29), int64(3050), int64(12500), "2026-10-11 09:00:00"}}},
|
||||||
|
testdb.Step{Match: "ORDER BY id DESC LIMIT 50", Columns: []string{"id"}},
|
||||||
|
testdb.Step{Match: "GROUP BY DATE_FORMAT", Columns: []string{"day", "count"}},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
c, _, _ := permissionCtx(t, "GET", "/api/smokingInfo?user_id=2", "", steps)
|
||||||
|
result, err := smokingSnapshot(c, 2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result["read_only"] != true {
|
||||||
|
t.Fatal("partner view must be read only")
|
||||||
|
}
|
||||||
|
expected := map[string]int64{"week_count": 0, "month_count": 0, "week_purchase_cents": 0, "month_purchase_cents": 0}
|
||||||
|
if wallet {
|
||||||
|
expected = map[string]int64{"week_count": 7, "month_count": 29, "week_purchase_cents": 3050, "month_purchase_cents": 12500}
|
||||||
|
}
|
||||||
|
for k, v := range expected {
|
||||||
|
if result[k] != v {
|
||||||
|
t.Fatalf("%s=%v want %d", k, result[k], v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,7 @@ const cache = require('../../utils/data-cache');
|
|||||||
const titles = {consume:'抽一支 · 打卡',purchase:'买烟',receive:'别人递烟',receive_smoke:'收到并抽一支',adjust:'调整库存',give:'递给别人一支',undo:'撤销记录'};
|
const titles = {consume:'抽一支 · 打卡',purchase:'买烟',receive:'别人递烟',receive_smoke:'收到并抽一支',adjust:'调整库存',give:'递给别人一支',undo:'撤销记录'};
|
||||||
Component({
|
Component({
|
||||||
properties: { detailed: Boolean },
|
properties: { detailed: Boolean },
|
||||||
data: { loading:true, error:false, info:null, saving:false, sheet:false, mode:'purchase', quantity:'1', packSize:'20', amount:'', note:'', viewUserId:0, lastLabel:'还没有记录', daily:[], logs:[], ownerName:'', ownerAvatar:'', ownerInitial:'我' },
|
data: { loading:true, error:false, info:null, saving:false, sheet:false, mode:'purchase', quantity:'1', packSize:'20', amount:'', note:'', viewUserId:0, lastLabel:'还没有记录', daily:[], logs:[], periodStats:[], ownerName:'', ownerAvatar:'', ownerInitial:'我' },
|
||||||
lifetimes: { attached(){this.load();} },
|
lifetimes: { attached(){this.load();} },
|
||||||
pageLifetimes: { show(){this.load();} },
|
pageLifetimes: { show(){this.load();} },
|
||||||
methods: {
|
methods: {
|
||||||
@@ -22,12 +22,17 @@ Component({
|
|||||||
const map={};(info.daily||[]).forEach(row=>{map[row.day]=Number(row.count);});const daily=[];
|
const map={};(info.daily||[]).forEach(row=>{map[row.day]=Number(row.count);});const daily=[];
|
||||||
for(let ago=6;ago>=0;ago--){const day=new Date();day.setDate(day.getDate()-ago);const key=day.getFullYear()+'-'+String(day.getMonth()+1).padStart(2,'0')+'-'+String(day.getDate()).padStart(2,'0');daily.push({day:key,label:ago===0?'今天':(day.getMonth()+1)+'/'+day.getDate(),count:map[key]||0});}
|
for(let ago=6;ago>=0;ago--){const day=new Date();day.setDate(day.getDate()-ago);const key=day.getFullYear()+'-'+String(day.getMonth()+1).padStart(2,'0')+'-'+String(day.getDate()).padStart(2,'0');daily.push({day:key,label:ago===0?'今天':(day.getMonth()+1)+'/'+day.getDate(),count:map[key]||0});}
|
||||||
const max=Math.max(1,...daily.map(day=>day.count));daily.forEach(day=>day.height=Math.max(4,day.count/max*100));
|
const max=Math.max(1,...daily.map(day=>day.count));daily.forEach(day=>day.height=Math.max(4,day.count/max*100));
|
||||||
|
const periodStats=[{key:'week',title:'本周',range:'周一至今'},{key:'month',title:'本月',range:'本月1日至今'}].map(period=>({
|
||||||
|
...period,
|
||||||
|
count:info[period.key+'_count'] == null ? '—' : Number(info[period.key+'_count']),
|
||||||
|
money:info[period.key+'_purchase_cents'] == null ? '—' : (Number(info[period.key+'_purchase_cents'])/100).toFixed(2)
|
||||||
|
}));
|
||||||
const profile=this._profile||{};
|
const profile=this._profile||{};
|
||||||
const ownerId=Number(info.user_id || this.data.viewUserId || wx.getStorageSync('userId'));
|
const ownerId=Number(info.user_id || this.data.viewUserId || wx.getStorageSync('userId'));
|
||||||
const owner=[profile.partner1,profile.partner2].find(user=>user && Number(user.id)===ownerId)||{};
|
const owner=[profile.partner1,profile.partner2].find(user=>user && Number(user.id)===ownerId)||{};
|
||||||
const ownerName=owner.nickname||info.owner_name||owner.username||(info.read_only?'TA':'我');
|
const ownerName=owner.nickname||info.owner_name||owner.username||(info.read_only?'TA':'我');
|
||||||
const ownerAvatar=getImageUrl(owner.avatar||info.owner_avatar||'');
|
const ownerAvatar=getImageUrl(owner.avatar||info.owner_avatar||'');
|
||||||
this.setData({info,logs,daily,lastLabel,ownerName,ownerAvatar,ownerInitial:ownerName.slice(0,1),error:false,loading:false});
|
this.setData({info,logs,daily,periodStats,lastLabel,ownerName,ownerAvatar,ownerInitial:ownerName.slice(0,1),error:false,loading:false});
|
||||||
},
|
},
|
||||||
avatarFailed(){this.setData({ownerAvatar:''});},
|
avatarFailed(){this.setData({ownerAvatar:''});},
|
||||||
retry(){this.load(true);},
|
retry(){this.load(true);},
|
||||||
|
|||||||
@@ -14,6 +14,9 @@
|
|||||||
</block>
|
</block>
|
||||||
<view wx:if="{{!detailed && logs.length && !info.read_only}}" class="smoke-recent"><text>{{logs[0].title}} · {{logs[0].time}}</text><text wx:if="{{!logs[0].undone && logs[0].kind !== 'undo'}}" data-id="{{logs[0].id}}" bindtap="undo">撤销</text></view>
|
<view wx:if="{{!detailed && logs.length && !info.read_only}}" class="smoke-recent"><text>{{logs[0].title}} · {{logs[0].time}}</text><text wx:if="{{!logs[0].undone && logs[0].kind !== 'undo'}}" data-id="{{logs[0].id}}" bindtap="undo">撤销</text></view>
|
||||||
<block wx:if="{{detailed}}">
|
<block wx:if="{{detailed}}">
|
||||||
|
<view class="section-title">抽烟与购买统计</view>
|
||||||
|
<view class="smoke-periods"><view class="smoke-period" wx:for="{{periodStats}}" wx:key="key"><view class="smoke-period-head"><text>{{item.title}}</text><text>{{item.range}}</text></view><view class="smoke-period-row"><text>已抽</text><text>{{item.count}} 支</text></view><view class="smoke-period-row"><text>买烟金额</text><text>¥{{item.money}}</text></view></view></view>
|
||||||
|
<text class="smoke-period-hint">购买金额按已填写金额累计,已撤销记录不计入</text>
|
||||||
<view class="section-title">最近七天<text>累计 {{info.total_count}} 支</text></view>
|
<view class="section-title">最近七天<text>累计 {{info.total_count}} 支</text></view>
|
||||||
<view class="week-chart"><view class="chart-day" wx:for="{{daily}}" wx:key="day"><text>{{item.count}}</text><view class="chart-track"><view class="chart-bar" style="height:{{item.height}}%" /></view><text>{{item.label}}</text></view></view>
|
<view class="week-chart"><view class="chart-day" wx:for="{{daily}}" wx:key="day"><text>{{item.count}}</text><view class="chart-track"><view class="chart-bar" style="height:{{item.height}}%" /></view><text>{{item.label}}</text></view></view>
|
||||||
<view class="section-title">库存与打卡流水<text>最近 50 条</text></view>
|
<view class="section-title">库存与打卡流水<text>最近 50 条</text></view>
|
||||||
|
|||||||
@@ -12,3 +12,5 @@
|
|||||||
.smoke-give-disabled{color:#A39386;opacity:.65}
|
.smoke-give-disabled{color:#A39386;opacity:.65}
|
||||||
|
|
||||||
@import '../../styles/typography.wxss';
|
@import '../../styles/typography.wxss';
|
||||||
|
|
||||||
|
.smoke-periods{display:flex;gap:16rpx}.smoke-period{flex:1;min-width:0;padding:22rpx 18rpx;border:1rpx solid #F1DCE4;border-radius:20rpx;background:#FFF5F8}.smoke-period-head{display:flex;align-items:center;justify-content:space-between;gap:8rpx;font-size:25rpx;color:#64424D}.smoke-period-head text+text{font-size:18rpx;color:#A39386}.smoke-period-row{display:flex;justify-content:space-between;gap:10rpx;font-size:23rpx;margin-top:18rpx;color:#85776D}.smoke-period-row text+text{color:#B74768;overflow-wrap:anywhere}.smoke-period-hint{display:block;margin-top:14rpx;font-size:20rpx;color:#A39386;line-height:1.6}
|
||||||
|
|||||||
@@ -45,3 +45,14 @@ test('giving a cigarette submits one stock operation, labels its ledger and reje
|
|||||||
e.component.data.info.stock=0;await e.component.giveSmoke();assert.equal(e.calls.length,1);assert.equal(e.toasts.at(-1).title,'库存不足,请先补充库存');
|
e.component.data.info.stock=0;await e.component.giveSmoke();assert.equal(e.calls.length,1);assert.equal(e.toasts.at(-1).title,'库存不足,请先补充库存');
|
||||||
e.component.data.info.stock=5;e.component.data.info.read_only=true;await e.component.giveSmoke();assert.equal(e.calls.length,1);
|
e.component.data.info.stock=5;e.component.data.info.read_only=true;await e.component.giveSmoke();assert.equal(e.calls.length,1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('weekly and monthly purchase totals use cents and refresh with selected owner',()=>{
|
||||||
|
const {component}=setup(async()=>result());
|
||||||
|
component.applyInfo({user_id:1,week_count:7,month_count:29,week_purchase_cents:3050,month_purchase_cents:12500,logs:[],daily:[]});
|
||||||
|
assert.equal(component.data.periodStats[0].count,7);assert.equal(component.data.periodStats[0].money,'30.50');
|
||||||
|
assert.equal(component.data.periodStats[1].count,29);assert.equal(component.data.periodStats[1].money,'125.00');
|
||||||
|
component.applyInfo({user_id:2,read_only:true,week_count:0,month_count:2,week_purchase_cents:0,month_purchase_cents:999,logs:[],daily:[]});
|
||||||
|
assert.equal(component.data.periodStats[0].money,'0.00');assert.equal(component.data.periodStats[1].money,'9.99');
|
||||||
|
assert.equal(component.data.periodStats[1].count,2);
|
||||||
|
component.applyInfo({logs:[],daily:[]});assert.equal(component.data.periodStats[0].count,'—');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user