chore: seed migration objects batch 52

This commit is contained in:
dev
2026-10-18 13:38:08 +08:00
parent f440b9e0e8
commit 042fd3f826
23 changed files with 793 additions and 99 deletions
+1
View File
@@ -20,4 +20,5 @@ var (
ErrRechargeOrderIDInvalid = errors.New("recharge order id invalid")
ErrRemarkInvalid = errors.New("remark invalid")
ErrVoucherInvalid = errors.New("voucher attachment ids invalid")
ErrLedgerFilterInvalid = errors.New("wallet ledger filter invalid")
)
+82 -7
View File
@@ -55,6 +55,7 @@ type Repository interface {
CreateRecharge(context.Context, store.RechargeCreation) (*store.RechargeOrder, error)
RechargeOrders(context.Context, int64, int64, int, int, int) (*store.RechargePage, error)
RechargeOrder(context.Context, int64, int64, int64) (*store.RechargeOrder, error)
WalletLedgers(context.Context, int64, int64, string, int, int, int) (*store.WalletLedgerPage, error)
}
type Service struct {
@@ -209,6 +210,29 @@ type RechargePage struct {
HasNext bool `json:"has_next"`
}
type WalletLedger struct {
ID int64 `json:"id"`
LedgerNo string `json:"ledger_no"`
BizType string `json:"biz_type"`
BizNo string `json:"biz_no"`
EntryType string `json:"entry_type"`
Direction string `json:"direction"`
Amount int64 `json:"amount"`
BeforeAmount int64 `json:"before_amount"`
AfterAmount int64 `json:"after_amount"`
Currency string `json:"currency"`
Remark string `json:"remark"`
OccurredAt string `json:"occurred_at"`
}
type WalletLedgerPage struct {
Items []WalletLedger `json:"items"`
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
HasNext bool `json:"has_next"`
}
func New(repository Repository, redisClient *redis.Client, cfg config.AuthConfig) *Service {
service := &Service{repository: repository, redis: redisClient, now: time.Now}
encryptionKey, encryptionErr := decodeKey(cfg.DataEncryptionKey)
@@ -231,7 +255,7 @@ func New(repository Repository, redisClient *redis.Client, cfg config.AuthConfig
}
func (s *Service) PaymentAccounts(ctx context.Context, tenantID, memberID int64) ([]PaymentAccount, error) {
if !s.ready {
if s.repository == nil {
return nil, ErrUnavailable
}
rows, err := s.repository.PaymentAccounts(ctx, tenantID, memberID)
@@ -368,7 +392,7 @@ func (s *Service) CreateWithdrawal(ctx context.Context, tenantID, memberID int64
}
func (s *Service) WithdrawalOrders(ctx context.Context, tenantID, memberID int64, page, pageSize int, status string) (*WithdrawalPage, error) {
if !s.ready {
if s.repository == nil {
return nil, ErrUnavailable
}
if page == 0 {
@@ -399,7 +423,7 @@ func (s *Service) WithdrawalOrders(ctx context.Context, tenantID, memberID int64
}
func (s *Service) WithdrawalOrder(ctx context.Context, tenantID, memberID, orderID int64) (*Withdrawal, error) {
if !s.ready {
if s.repository == nil {
return nil, ErrUnavailable
}
if orderID <= 0 {
@@ -414,7 +438,7 @@ func (s *Service) WithdrawalOrder(ctx context.Context, tenantID, memberID, order
}
func (s *Service) RechargeChannels(ctx context.Context, tenantID int64) ([]RechargeChannel, error) {
if !s.ready {
if s.repository == nil {
return nil, ErrUnavailable
}
rows, err := s.repository.RechargeChannels(ctx, tenantID)
@@ -432,7 +456,7 @@ func (s *Service) RechargeChannels(ctx context.Context, tenantID int64) ([]Recha
}
func (s *Service) CreateRecharge(ctx context.Context, tenantID, memberID int64, input CreateRechargeInput) (*Recharge, error) {
if !s.ready {
if s.repository == nil {
return nil, ErrUnavailable
}
input.Currency = strings.ToUpper(strings.TrimSpace(input.Currency))
@@ -495,7 +519,7 @@ func (s *Service) CreateRecharge(ctx context.Context, tenantID, memberID int64,
}
func (s *Service) RechargeOrders(ctx context.Context, tenantID, memberID int64, page, pageSize int, status string) (*RechargePage, error) {
if !s.ready {
if s.repository == nil {
return nil, ErrUnavailable
}
if page == 0 {
@@ -527,7 +551,7 @@ func (s *Service) RechargeOrders(ctx context.Context, tenantID, memberID int64,
}
func (s *Service) RechargeOrder(ctx context.Context, tenantID, memberID, orderID int64) (*Recharge, error) {
if !s.ready {
if s.repository == nil {
return nil, ErrUnavailable
}
if orderID <= 0 {
@@ -541,6 +565,57 @@ func (s *Service) RechargeOrder(ctx context.Context, tenantID, memberID, orderID
return &result, nil
}
func (s *Service) WalletLedgers(ctx context.Context, tenantID, memberID int64, page, pageSize int, bizType, direction string) (*WalletLedgerPage, error) {
if s.repository == nil {
return nil, ErrUnavailable
}
if page == 0 {
page = 1
}
if pageSize == 0 {
pageSize = defaultPageSize
}
if page < 1 {
return nil, ErrPageInvalid
}
if pageSize < 1 || pageSize > maxPageSize {
return nil, ErrPageSizeInvalid
}
bizType = strings.TrimSpace(bizType)
if len(bizType) > 32 {
return nil, ErrLedgerFilterInvalid
}
directionCode := 0
switch strings.ToLower(strings.TrimSpace(direction)) {
case "", "all":
case "income":
directionCode = 1
case "expense":
directionCode = 2
default:
return nil, ErrLedgerFilterInvalid
}
rows, err := s.repository.WalletLedgers(ctx, tenantID, memberID, bizType, directionCode, (page-1)*pageSize, pageSize)
if err != nil {
return nil, err
}
items := make([]WalletLedger, 0, len(rows.Items))
for _, row := range rows.Items {
directionName := "expense"
if row.Direction == 1 {
directionName = "income"
}
items = append(items, WalletLedger{
ID: row.ID, LedgerNo: row.LedgerNo, BizType: row.BizType, BizNo: row.BizNo,
EntryType: row.EntryType, Direction: directionName, Amount: row.Amount,
BeforeAmount: row.BeforeAmount, AfterAmount: row.AfterAmount, Currency: row.Currency,
Remark: row.Remark, OccurredAt: row.OccurredAt,
})
}
return &WalletLedgerPage{Items: items, Page: page, PageSize: pageSize, Total: rows.Total,
HasNext: int64(page*pageSize) < rows.Total}, nil
}
func (s *Service) quote(ctx context.Context, tenantID, memberID int64, input QuoteInput) (*Quote, error) {
quote, _, _, err := s.quoteData(ctx, tenantID, memberID, input)
return quote, err
+42 -1
View File
@@ -19,6 +19,7 @@ type fakeRepository struct {
orders []store.WithdrawalOrder
rechargeChannels []store.RechargeChannel
rechargeOrders []store.RechargeOrder
ledgers []store.WalletLedger
createdRecharge store.RechargeCreation
}
@@ -67,6 +68,9 @@ func (f *fakeRepository) RechargeOrders(context.Context, int64, int64, int, int,
func (f *fakeRepository) RechargeOrder(context.Context, int64, int64, int64) (*store.RechargeOrder, error) {
return nil, errors.New("not implemented")
}
func (f *fakeRepository) WalletLedgers(context.Context, int64, int64, string, int, int, int) (*store.WalletLedgerPage, error) {
return &store.WalletLedgerPage{Items: f.ledgers, Total: int64(len(f.ledgers))}, nil
}
func TestQuoteCalculatesRateFeeAndLimits(t *testing.T) {
repository := &fakeRepository{config: json.RawMessage(`{"features":{"withdrawal_enabled":true}}`),
@@ -127,7 +131,7 @@ func TestCreateRechargeValidatesChannelAndMapsOrder(t *testing.T) {
repository := &fakeRepository{rechargeChannels: []store.RechargeChannel{{
ID: 4, Code: "manual", Name: "人工审核", MinAmount: 100, MaxAmount: 100000,
}}}
service := &Service{repository: repository, ready: true, now: time.Now}
service := New(repository, nil, config.AuthConfig{})
result, err := service.CreateRecharge(context.Background(), 0, 9, CreateRechargeInput{
ChannelID: 4, Amount: 50000, Currency: "cny", Remark: "测试",
})
@@ -157,6 +161,43 @@ func TestRechargeOrdersMapsStatusAndPagination(t *testing.T) {
}
}
func TestFinanceOrderListsDoNotRequireEncryptionKeys(t *testing.T) {
repository := &fakeRepository{
orders: []store.WithdrawalOrder{{ID: 2, WithdrawalNo: "WD2", Status: 1}},
rechargeOrders: []store.RechargeOrder{{ID: 3, RechargeNo: "RC3", Status: 1}},
}
service := New(repository, nil, config.AuthConfig{})
withdrawals, err := service.WithdrawalOrders(context.Background(), 0, 9, 1, 20, "all")
if err != nil || len(withdrawals.Items) != 1 {
t.Fatalf("withdrawal records should remain readable without encryption keys: %#v, %v", withdrawals, err)
}
recharges, err := service.RechargeOrders(context.Background(), 0, 9, 1, 20, "all")
if err != nil || len(recharges.Items) != 1 {
t.Fatalf("recharge records should remain readable without encryption keys: %#v, %v", recharges, err)
}
if _, err = service.PaymentAccounts(context.Background(), 0, 9); err != nil {
t.Fatalf("masked payment accounts should remain readable without encryption keys, got %v", err)
}
if _, err = service.CreatePaymentAccount(context.Background(), 0, 9, PaymentAccountInput{}); !errors.Is(err, ErrUnavailable) {
t.Fatalf("sensitive finance operations must remain unavailable without encryption keys, got %v", err)
}
}
func TestWalletLedgersDoNotRequireEncryptionKeys(t *testing.T) {
service := New(&fakeRepository{ledgers: []store.WalletLedger{{
ID: 7, LedgerNo: "WL7", Direction: 1, Amount: 947, Remark: "下级交易返佣",
}}}, nil, config.AuthConfig{})
result, err := service.WalletLedgers(context.Background(), 0, 9, 1, 20, "", "all")
if err != nil || result.Total != 1 || len(result.Items) != 1 ||
result.Items[0].Direction != "income" || result.Items[0].Amount != 947 {
t.Fatalf("wallet ledgers should remain readable without encryption keys: %#v, %v", result, err)
}
if _, err = service.WalletLedgers(context.Background(), 0, 9, 1, 20, "", "sideways"); !errors.Is(err, ErrLedgerFilterInvalid) {
t.Fatalf("expected invalid ledger direction, got %v", err)
}
}
func TestDeterministicNumberIsStableAndScoped(t *testing.T) {
first := deterministicNumber("WD", 0, 1, "request-key")
if first != deterministicNumber("WD", 0, 1, "request-key") || first == deterministicNumber("WD", 0, 2, "request-key") {
+2
View File
@@ -38,6 +38,7 @@ func (a *API) Register(mux *http.ServeMux) {
mux.HandleFunc("/v1/member-levels", a.memberLevels)
mux.HandleFunc("/v1/leaderboard", a.leaderboard)
mux.HandleFunc("/v1/customer-services", a.customerServices)
mux.HandleFunc("/v1/notices", a.notices)
mux.HandleFunc("/v1/task-halls", a.taskHalls)
mux.HandleFunc("/v1/tasks/eligibility", a.taskEligibility)
mux.HandleFunc("/v1/tasks/stats", a.taskStats)
@@ -47,6 +48,7 @@ func (a *API) Register(mux *http.ServeMux) {
mux.HandleFunc("/v1/manual-products", a.manualProducts)
mux.HandleFunc("/v1/manual-products/", a.manualProduct)
mux.HandleFunc("/v1/payment-accounts", a.paymentAccounts)
mux.HandleFunc("/v1/wallet/ledgers", a.walletLedgers)
mux.HandleFunc("/v1/recharge/channels", a.rechargeChannels)
mux.HandleFunc("/v1/recharge-orders", a.rechargeOrders)
mux.HandleFunc("/v1/recharge-orders/", a.rechargeOrder)
+30
View File
@@ -41,6 +41,34 @@ func (a *API) paymentAccounts(w http.ResponseWriter, r *http.Request) {
writeSuccess(w, r, result)
}
func (a *API) walletLedgers(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
methodNotAllowed(w, r, http.MethodGet)
return
}
principal, ok := a.authenticateFinanceMember(w, r)
if !ok {
return
}
page, ok := optionalPositiveInt(w, r, "page")
if !ok {
writeFinanceError(w, r, finance.ErrPageInvalid)
return
}
pageSize, ok := optionalPositiveInt(w, r, "page_size")
if !ok {
writeFinanceError(w, r, finance.ErrPageSizeInvalid)
return
}
result, err := a.finance.WalletLedgers(r.Context(), principal.TenantID, principal.MemberID,
page, pageSize, r.URL.Query().Get("biz_type"), r.URL.Query().Get("direction"))
if err != nil {
writeFinanceError(w, r, err)
return
}
writeSuccess(w, r, result)
}
func (a *API) withdrawalQuote(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
methodNotAllowed(w, r, http.MethodPost)
@@ -256,6 +284,8 @@ func writeFinanceError(w http.ResponseWriter, r *http.Request, err error) {
writeError(w, r, http.StatusBadRequest, 30000, "PAGE_INVALID", "validation failed", map[string]any{"field": "page"})
case errors.Is(err, finance.ErrPageSizeInvalid):
writeError(w, r, http.StatusBadRequest, 30000, "PAGE_SIZE_INVALID", "validation failed", map[string]any{"field": "page_size"})
case errors.Is(err, finance.ErrLedgerFilterInvalid):
writeError(w, r, http.StatusBadRequest, 30000, "LEDGER_FILTER_INVALID", "validation failed", nil)
case errors.Is(err, finance.ErrStatusInvalid):
writeError(w, r, http.StatusBadRequest, 30000, "WITHDRAWAL_STATUS_INVALID", "validation failed", map[string]any{"field": "status"})
case errors.Is(err, finance.ErrRechargeStatusInvalid):
+34
View File
@@ -157,6 +157,36 @@ func (a *API) customerServices(w http.ResponseWriter, r *http.Request) {
writeSuccess(w, r, items)
}
func (a *API) notices(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
methodNotAllowed(w, r, http.MethodGet)
return
}
if !a.ensurePortal(w, r) {
return
}
principal, ok := a.authenticateMember(w, r)
if !ok {
return
}
page, ok := optionalPositiveInt(w, r, "page")
if !ok {
writePortalError(w, r, portal.ErrPageInvalid)
return
}
pageSize, ok := optionalPositiveInt(w, r, "page_size")
if !ok {
writePortalError(w, r, portal.ErrPageSizeInvalid)
return
}
result, err := a.portal.Notices(r.Context(), principal.TenantID, principal.MemberID, page, pageSize)
if err != nil {
writePortalError(w, r, err)
return
}
writeSuccess(w, r, result)
}
func (a *API) ensurePortal(w http.ResponseWriter, r *http.Request) bool {
if a.portal != nil {
return true
@@ -171,6 +201,10 @@ func writePortalError(w http.ResponseWriter, r *http.Request, err error) {
writeError(w, r, http.StatusBadRequest, 30000, "BANNER_POSITION_INVALID", "validation failed", map[string]any{"field": "position"})
case errors.Is(err, portal.ErrContentKeyInvalid):
writeError(w, r, http.StatusBadRequest, 30000, "CONTENT_KEY_INVALID", "validation failed", map[string]any{"field": "content_key"})
case errors.Is(err, portal.ErrPageInvalid):
writeError(w, r, http.StatusBadRequest, 30000, "PAGE_INVALID", "validation failed", map[string]any{"field": "page"})
case errors.Is(err, portal.ErrPageSizeInvalid):
writeError(w, r, http.StatusBadRequest, 30000, "PAGE_SIZE_INVALID", "validation failed", map[string]any{"field": "page_size"})
case errors.Is(err, store.ErrResourceNotFound):
writeError(w, r, http.StatusNotFound, 30044, "RESOURCE_NOT_FOUND", "resource not found", nil)
default:
+2
View File
@@ -5,4 +5,6 @@ import "errors"
var (
ErrPositionInvalid = errors.New("banner position invalid")
ErrContentKeyInvalid = errors.New("content key invalid")
ErrPageInvalid = errors.New("page invalid")
ErrPageSizeInvalid = errors.New("page size invalid")
)
+41 -6
View File
@@ -22,6 +22,7 @@ type Repository interface {
PublishedContent(context.Context, int64, string) (*store.Content, error)
ActiveMemberLevels(context.Context, int64) ([]store.MemberLevel, error)
ActiveMemberNotices(context.Context, int64, int64, int) ([]store.MemberNotice, error)
MemberNotices(context.Context, int64, int64, int, int) (*store.MemberNoticePage, error)
MemberTaskSummary(context.Context, int64, int64) (*store.TaskSummary, error)
TaskLeaderboard(context.Context, int64, int) ([]store.LeaderboardMember, error)
ActiveCustomerServices(context.Context, int64) ([]store.CustomerService, error)
@@ -53,6 +54,14 @@ type Notice struct {
SourceType string `json:"source_type"`
StartAt string `json:"start_at"`
EndAt string `json:"end_at"`
CreatedAt string `json:"created_at"`
}
type NoticePage struct {
Items []Notice `json:"items"`
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
}
type Home struct {
@@ -168,6 +177,32 @@ func (s *Service) Banners(ctx context.Context, tenantID int64, position string)
return items, nil
}
func (s *Service) Notices(ctx context.Context, tenantID, memberID int64, page, pageSize int) (*NoticePage, error) {
if page == 0 {
page = 1
}
if pageSize == 0 {
pageSize = 20
}
if page < 1 {
return nil, ErrPageInvalid
}
if pageSize < 1 || pageSize > 100 {
return nil, ErrPageSizeInvalid
}
rows, err := s.repository.MemberNotices(ctx, tenantID, memberID, (page-1)*pageSize, pageSize)
if err != nil {
return nil, err
}
items := make([]Notice, 0, len(rows.Items))
for _, row := range rows.Items {
items = append(items, Notice{ID: row.ID, Title: row.Title, Content: row.Content,
SourceType: noticeSourceName(row.SourceType), StartAt: row.StartAt, EndAt: row.EndAt,
CreatedAt: row.CreatedAt})
}
return &NoticePage{Items: items, Page: page, PageSize: pageSize, Total: rows.Total}, nil
}
func (s *Service) Content(ctx context.Context, tenantID int64, key string) (*Content, error) {
key = strings.ToLower(strings.TrimSpace(key))
if !contentKeyPattern.MatchString(key) {
@@ -256,14 +291,14 @@ func parseFeatures(raw json.RawMessage) Features {
return Features{}
}
return Features{
TaskEnabled: findBool(config, true, "task_enabled", "taskEnabled", "TASK"),
RechargeEnabled: findBool(config, true, "recharge_enabled", "rechargeEnabled", "RECHARGE"),
WithdrawalEnabled: findBool(config, true, "withdrawal_enabled", "withdrawalEnabled", "WITHDRAWAL"),
ManualMallEnabled: findBool(config, true, "manual_mall_enabled", "manualMallEnabled", "SHOP_STATUS"),
TaskEnabled: findBool(config, "task_enabled", "taskEnabled", "TASK"),
RechargeEnabled: findBool(config, "recharge_enabled", "rechargeEnabled", "RECHARGE"),
WithdrawalEnabled: findBool(config, "withdrawal_enabled", "withdrawalEnabled", "WITHDRAWAL"),
ManualMallEnabled: findBool(config, "manual_mall_enabled", "manualMallEnabled", "SHOP_STATUS"),
}
}
func findBool(config map[string]any, fallback bool, keys ...string) bool {
func findBool(config map[string]any, keys ...string) bool {
for _, key := range keys {
if value, ok := config[key]; ok {
return boolValue(value)
@@ -276,7 +311,7 @@ func findBool(config map[string]any, fallback bool, keys ...string) bool {
}
}
}
return fallback
return false
}
func boolValue(value any) bool {
+37 -15
View File
@@ -13,14 +13,15 @@ import (
)
type fakeRepository struct {
config json.RawMessage
banners []store.Banner
content *store.Content
levels []store.MemberLevel
notices []store.MemberNotice
summary *store.TaskSummary
leaders []store.LeaderboardMember
services []store.CustomerService
config json.RawMessage
banners []store.Banner
content *store.Content
levels []store.MemberLevel
notices []store.MemberNotice
summary *store.TaskSummary
leaders []store.LeaderboardMember
services []store.CustomerService
noticePage *store.MemberNoticePage
}
func (r fakeRepository) CurrentSiteConfig(context.Context, int64) (json.RawMessage, error) {
@@ -41,6 +42,12 @@ func (r fakeRepository) ActiveMemberLevels(context.Context, int64) ([]store.Memb
func (r fakeRepository) ActiveMemberNotices(context.Context, int64, int64, int) ([]store.MemberNotice, error) {
return r.notices, nil
}
func (r fakeRepository) MemberNotices(context.Context, int64, int64, int, int) (*store.MemberNoticePage, error) {
if r.noticePage == nil {
return &store.MemberNoticePage{Items: []store.MemberNotice{}}, nil
}
return r.noticePage, nil
}
func (r fakeRepository) MemberTaskSummary(context.Context, int64, int64) (*store.TaskSummary, error) {
return r.summary, nil
}
@@ -61,13 +68,6 @@ func TestParseFeaturesSupportsNestedAndLegacyKeys(t *testing.T) {
}
}
func TestParseFeaturesDefaultsToEnabled(t *testing.T) {
features := parseFeatures(json.RawMessage(`{"siteName":"demo"}`))
if !features.TaskEnabled || !features.RechargeEnabled || !features.WithdrawalEnabled || !features.ManualMallEnabled {
t.Fatalf("unexpected feature defaults: %+v", features)
}
}
func TestBannersSanitizeTargets(t *testing.T) {
service := New(fakeRepository{banners: []store.Banner{
{ID: 1, TargetType: 2, TargetValue: "/receive"},
@@ -171,3 +171,25 @@ func TestCustomerServicesSanitizeURLsAndMapStatus(t *testing.T) {
t.Fatalf("unexpected customer services: %+v", items)
}
}
func TestNoticesMapsPageAndSourceType(t *testing.T) {
service := New(fakeRepository{noticePage: &store.MemberNoticePage{
Items: []store.MemberNotice{{ID: 3, Title: "活动通知", SourceType: 2, CreatedAt: "2026-10-13T08:00:00Z"}},
Total: 21,
}})
page, err := service.Notices(context.Background(), 1, 7, 2, 20)
if err != nil {
t.Fatal(err)
}
if page.Page != 2 || page.PageSize != 20 || page.Total != 21 || len(page.Items) != 1 ||
page.Items[0].SourceType != "operational_simulation" || page.Items[0].CreatedAt == "" {
t.Fatalf("unexpected notice page: %+v", page)
}
}
func TestNoticesRejectsOversizedPage(t *testing.T) {
service := New(fakeRepository{})
if _, err := service.Notices(context.Background(), 1, 7, 1, 101); !errors.Is(err, ErrPageSizeInvalid) {
t.Fatalf("error = %v, want %v", err, ErrPageSizeInvalid)
}
}
+29 -1
View File
@@ -53,6 +53,12 @@ type MemberNotice struct {
SourceType int `db:"source_type"`
StartAt string `db:"start_at"`
EndAt string `db:"end_at"`
CreatedAt string `db:"created_at"`
}
type MemberNoticePage struct {
Items []MemberNotice
Total int64
}
type TaskSummary struct {
@@ -147,13 +153,35 @@ func (s *Store) ActiveMemberNotices(ctx context.Context, tenantID, memberID int6
rows := make([]MemberNotice, 0)
err := s.db.SelectContext(ctx, &rows, `SELECT id,title,content,source_type,
DATE_FORMAT(start_at,'%Y-%m-%dT%H:%i:%sZ') AS start_at,
DATE_FORMAT(end_at,'%Y-%m-%dT%H:%i:%sZ') AS end_at
DATE_FORMAT(end_at,'%Y-%m-%dT%H:%i:%sZ') AS end_at,
DATE_FORMAT(created_at,'%Y-%m-%dT%H:%i:%sZ') AS created_at
FROM hg_app_member_notice WHERE tenant_id=? AND member_id IN (0,?) AND notice_status=1
AND start_at<=UTC_TIMESTAMP(3) AND end_at>UTC_TIMESTAMP(3)
ORDER BY start_at DESC,id DESC LIMIT ?`, tenantID, memberID, limit)
return rows, err
}
func (s *Store) MemberNotices(ctx context.Context, tenantID, memberID int64, offset, limit int) (*MemberNoticePage, error) {
if s == nil || s.db == nil {
return nil, errors.New("store is unavailable")
}
const where = ` WHERE tenant_id=? AND member_id IN (0,?) AND notice_status=1
AND start_at<=UTC_TIMESTAMP(3) AND end_at>UTC_TIMESTAMP(3)`
args := []any{tenantID, memberID}
var total int64
if err := s.db.GetContext(ctx, &total, `SELECT COUNT(*) FROM hg_app_member_notice`+where, args...); err != nil {
return nil, err
}
items := make([]MemberNotice, 0)
args = append(args, limit, offset)
err := s.db.SelectContext(ctx, &items, `SELECT id,title,content,source_type,
DATE_FORMAT(start_at,'%Y-%m-%dT%H:%i:%sZ') AS start_at,
DATE_FORMAT(end_at,'%Y-%m-%dT%H:%i:%sZ') AS end_at,
DATE_FORMAT(created_at,'%Y-%m-%dT%H:%i:%sZ') AS created_at
FROM hg_app_member_notice`+where+` ORDER BY created_at DESC,id DESC LIMIT ? OFFSET ?`, args...)
return &MemberNoticePage{Items: items, Total: total}, err
}
func (s *Store) MemberTaskSummary(ctx context.Context, tenantID, memberID int64) (*TaskSummary, error) {
if s == nil || s.db == nil {
return nil, errors.New("store is unavailable")
+50
View File
@@ -0,0 +1,50 @@
package store
import (
"context"
)
type WalletLedger struct {
ID int64 `db:"id"`
LedgerNo string `db:"ledger_no"`
BizType string `db:"biz_type"`
BizNo string `db:"biz_no"`
EntryType string `db:"entry_type"`
Direction int `db:"direction"`
Amount int64 `db:"amount"`
BeforeAmount int64 `db:"before_amount"`
AfterAmount int64 `db:"after_amount"`
Currency string `db:"currency"`
Remark string `db:"remark"`
OccurredAt string `db:"occurred_at"`
}
type WalletLedgerPage struct {
Items []WalletLedger
Total int64
}
func (s *Store) WalletLedgers(ctx context.Context, tenantID, memberID int64, bizType string, direction, offset, limit int) (*WalletLedgerPage, error) {
where := ` WHERE tenant_id=? AND member_id=?`
args := []any{tenantID, memberID}
if bizType != "" {
where += ` AND biz_type=?`
args = append(args, bizType)
}
if direction > 0 {
where += ` AND direction=?`
args = append(args, direction)
}
var total int64
if err := s.db.GetContext(ctx, &total, `SELECT COUNT(*) FROM hg_wallet_ledger`+where, args...); err != nil {
return nil, err
}
args = append(args, limit, offset)
items := make([]WalletLedger, 0)
err := s.db.SelectContext(ctx, &items, `SELECT id,ledger_no,biz_type,biz_no,entry_type,direction,
amount,before_amount,after_amount,currency,remark,
DATE_FORMAT(occurred_at,'%Y-%m-%dT%H:%i:%sZ') AS occurred_at
FROM hg_wallet_ledger`+where+` ORDER BY occurred_at DESC,id DESC LIMIT ? OFFSET ?`, args...)
return &WalletLedgerPage{Items: items, Total: total}, err
}