2026-08-04 18:24:39 +08:00
package tasks
import (
"context"
"database/sql"
"errors"
"strings"
"time"
"cmbuyer/admin/internal/domain"
)
// ErrInvalidFilter 表示任务筛选值无效,路由应按字段重新渲染而不是泄露内部错误。
var ErrInvalidFilter = errors . New ( "invalid task filter" )
// SetStartPolicy is called during startup; policy is explicit because authorization limits must not be implicit defaults.
func ( store * SQLiteStore ) SetStartPolicy ( policy StartPolicy ) { store . policy = policy }
func ( store * SQLiteStore ) ListTasks ( ctx context . Context , filter TaskFilter ) ([] TaskRow , error ) {
if ! ValidateTaskFilter ( filter ). Valid () {
return nil , ErrInvalidFilter
}
from , to , err := ShanghaiRange ( filter . CreatedFrom , filter . CreatedTo )
if err != nil {
return nil , ErrInvalidFilter
}
clauses , args := [] string { "1=1" }, [] any {}
if filter . Status != "" {
clauses = append ( clauses , "status = ?" )
args = append ( args , filter . Status )
}
if filter . Keyword != "" {
escaped := strings . NewReplacer ( "\\" , "\\\\" , "%" , "\\%" , "_" , "\\_" ). Replace ( filter . Keyword )
clauses = append ( clauses , "(title LIKE ? ESCAPE '\\' OR goods_id LIKE ? ESCAPE '\\')" )
args = append ( args , "%" + escaped + "%" , "%" + escaped + "%" )
}
if ! from . IsZero () {
clauses = append ( clauses , "julianday(created_at) >= julianday(?)" )
args = append ( args , from . Format ( time . RFC3339Nano ))
}
if ! to . IsZero () {
clauses = append ( clauses , "julianday(created_at) < julianday(?)" )
args = append ( args , to . Format ( time . RFC3339Nano ))
}
rows , err := store . database . QueryContext ( ctx , "SELECT id,title,goods_id,sku_color,sku_size,quantity,max_total_price,status,version,created_at FROM tasks WHERE " + strings . Join ( clauses , " AND " ) + " ORDER BY julianday(created_at) DESC,rowid DESC" , args ... )
if err != nil {
return nil , err
}
defer rows . Close ()
result := [] TaskRow {}
for rows . Next () {
var item TaskRow
var created string
if err := rows . Scan ( & item . ID , & item . Title , & item . GoodsID , & item . SKUColor , & item . SKUSize , & item . Quantity , & item . MaxTotalPrice , & item . Status , & item . Version , & created ); err != nil {
return nil , err
}
item . CreatedAt , err = time . Parse ( time . RFC3339Nano , created )
if err != nil {
return nil , err
}
result = append ( result , item )
}
return result , rows . Err ()
}
// ValidateTaskFilter 返回可关联到字段的错误,使服务端页面拒绝篡改参数时仍能保留输入值。
func ValidateTaskFilter ( filter TaskFilter ) Errors {
validation := Errors {}
if ! validTaskStatus ( filter . Status ) {
validation [ "status" ] = "请选择有效的任务状态。"
}
location , err := time . LoadLocation ( "Asia/Shanghai" )
if err != nil {
validation [ "created_from" ] = "日期筛选暂不可用,请稍后重试。"
validation [ "created_to" ] = "日期筛选暂不可用,请稍后重试。"
return validation
}
parseDate := func ( field , value string ) ( time . Time , bool ) {
if value == "" {
return time . Time {}, true
}
parsed , parseErr := time . ParseInLocation ( "2006-01-02" , value , location )
if parseErr != nil {
validation [ field ] = "请输入有效日期。"
return time . Time {}, false
}
return parsed , true
}
from , fromOK := parseDate ( "created_from" , filter . CreatedFrom )
to , toOK := parseDate ( "created_to" , filter . CreatedTo )
if fromOK && toOK && ! from . IsZero () && ! to . IsZero () && from . After ( to ) {
validation [ "created_to" ] = "结束日期不能早于开始日期。"
}
return validation
}
func ( store * SQLiteStore ) StartPurchases ( ctx context . Context , command StartCommand , createdBy string ) ( StartResult , error ) {
items , err := startItems ( command )
if err != nil || createdBy == "" {
return StartResult {}, ErrInvalidStart
}
if store . policy . AuthorizationTTL <= 0 || store . policy . MaxQuantity <= 0 {
return StartResult {}, ErrInvalidStart
}
_ , ceiling , ok := normalizeCents ( store . policy . MaxTotalPrice )
if ! ok {
return StartResult {}, ErrInvalidStart
}
writeCtx , cancel := context . WithTimeout ( ctx , sqliteWriteTimeout )
defer cancel ()
select {
case store . writeGate <- struct {}{}:
defer func () { <- store . writeGate }()
case <- writeCtx . Done ():
return StartResult {}, writeCtx . Err ()
}
tx , err := store . database . BeginTx ( writeCtx , nil )
if err != nil {
return StartResult {}, err
}
defer tx . Rollback ()
// Replay precedes any DRAFT check. One service process serializes this check with creation; SQLite uniqueness remains the cross-transaction backstop.
result , found , err := replayStart ( writeCtx , tx , command . StartKey , items )
if err != nil {
return StartResult {}, err
}
if found {
if err := tx . Commit (); err != nil {
return StartResult {}, err
}
return result , nil
}
now := store . now (). UTC ()
expires := now . Add ( store . policy . AuthorizationTTL )
result = StartResult { StartKey : command . StartKey , AuthorizedCount : len ( items ), Tasks : make ([] AuthorizedTask , 0 , len ( items )), PaymentAutomated : false }
for _ , item := range items {
var title , goods , color , size , price , status string
var quantity , version int
if err := tx . QueryRowContext ( writeCtx , "SELECT title,goods_id,sku_color,sku_size,quantity,max_total_price,status,version FROM tasks WHERE id=?" , item . TaskID ). Scan ( & title , & goods , & color , & size , & quantity , & price , & status , & version ); err != nil {
if err == sql . ErrNoRows {
return StartResult {}, ErrStartConflict
}
return StartResult {}, err
}
2026-08-05 01:25:23 +08:00
if status != "DRAFT" || version != item . ExpectedTaskVersion ||
! ValidTaskWireFields ( title , goods , color , size , price ) ||
quantity < 1 || quantity > store . policy . MaxQuantity {
2026-08-04 18:24:39 +08:00
return StartResult {}, ErrStartConflict
}
canonical , cents , ok := normalizeCents ( price )
if ! ok || canonical != price || cents . Cmp ( ceiling ) > 0 {
return StartResult {}, ErrStartConflict
}
if _ , err := domain . TransitionTask ( domain . TaskStatusDraft , domain . TaskStatusPending ); err != nil {
return StartResult {}, err
}
id , err := NewCreateKey ()
if err != nil {
return StartResult {}, err
}
next := version + 1
if _ , err = tx . ExecContext ( writeCtx , "INSERT INTO order_authorizations (id,task_id,task_version,start_key,goods_id,sku_color,sku_size,quantity,total_price_cap,status,created_by,created_at,expires_at) VALUES (?,?,?,?,?,?,?,?,?,'ACTIVE',?,?,?)" , id , item . TaskID , next , command . StartKey , goods , color , size , quantity , price , createdBy , now . Format ( time . RFC3339Nano ), expires . Format ( time . RFC3339Nano )); err != nil {
return StartResult {}, err
}
updated , err := tx . ExecContext ( writeCtx , "UPDATE tasks SET status='PENDING',version=version+1,updated_at=? WHERE id=? AND status='DRAFT' AND version=?" , now . Format ( time . RFC3339Nano ), item . TaskID , version )
if err != nil {
return StartResult {}, err
}
affected , err := updated . RowsAffected ()
if err != nil {
return StartResult {}, err
}
if affected != 1 {
return StartResult {}, ErrStartConflict
}
result . Tasks = append ( result . Tasks , AuthorizedTask { TaskID : item . TaskID , TaskVersion : next , AuthorizationID : id , ExpiresAt : expires })
}
if err := tx . Commit (); err != nil {
return StartResult {}, err
}
return result , nil
}
func replayStart ( ctx context . Context , tx * sql . Tx , startKey string , items [] StartItem ) ( StartResult , bool , error ) {
2026-08-05 01:25:23 +08:00
rows , err := tx . QueryContext ( ctx , `SELECT authorizations.id,authorizations.task_id,
authorizations.task_version,authorizations.expires_at,authorizations.goods_id,
authorizations.sku_color,authorizations.sku_size,authorizations.quantity,
authorizations.total_price_cap,tasks.title,tasks.goods_id,tasks.sku_color,
tasks.sku_size,tasks.quantity,tasks.max_total_price
FROM order_authorizations AS authorizations
JOIN tasks ON tasks.id = authorizations.task_id
WHERE authorizations.start_key=? ORDER BY authorizations.task_id` , startKey )
2026-08-04 18:24:39 +08:00
if err != nil {
return StartResult {}, false , err
}
defer rows . Close ()
result := StartResult { StartKey : startKey , PaymentAutomated : false }
for rows . Next () {
var item AuthorizedTask
var expires string
2026-08-05 01:25:23 +08:00
var authorizationGoodsID , authorizationColor , authorizationSize , authorizationPrice string
var taskTitle , taskGoodsID , taskColor , taskSize , taskPrice string
var authorizationQuantity , taskQuantity int
if err := rows . Scan ( & item . AuthorizationID , & item . TaskID , & item . TaskVersion , & expires ,
& authorizationGoodsID , & authorizationColor , & authorizationSize , & authorizationQuantity ,
& authorizationPrice , & taskTitle , & taskGoodsID , & taskColor , & taskSize , & taskQuantity ,
& taskPrice ); err != nil {
2026-08-04 18:24:39 +08:00
return StartResult {}, false , err
}
2026-08-05 01:25:23 +08:00
if ! ValidAuthorizationFields ( authorizationGoodsID , authorizationColor , authorizationSize , authorizationPrice ) ||
authorizationQuantity <= 0 ||
! ValidTaskWireFields ( taskTitle , taskGoodsID , taskColor , taskSize , taskPrice ) || taskQuantity <= 0 ||
authorizationGoodsID != taskGoodsID || authorizationColor != taskColor ||
authorizationSize != taskSize || authorizationQuantity != taskQuantity || authorizationPrice != taskPrice {
return StartResult {}, false , ErrStartConflict
}
2026-08-04 18:24:39 +08:00
item . ExpiresAt , err = time . Parse ( time . RFC3339Nano , expires )
if err != nil {
return StartResult {}, false , err
}
result . Tasks = append ( result . Tasks , item )
}
if err := rows . Err (); err != nil {
return StartResult {}, false , err
}
if len ( result . Tasks ) == 0 {
return StartResult {}, false , nil
}
if len ( result . Tasks ) != len ( items ) {
return StartResult {}, false , ErrStartConflict
}
for i := range items {
if result . Tasks [ i ]. TaskID != items [ i ]. TaskID || result . Tasks [ i ]. TaskVersion - 1 != items [ i ]. ExpectedTaskVersion {
return StartResult {}, false , ErrStartConflict
}
}
result . AuthorizedCount = len ( result . Tasks )
return result , true , nil
}