支付原来是个占位:调一下接口任务就算"已付"。现在任务和喂养者保证金都 走会员那套订单与支付网关,一笔订单买到什么由 applyPaidOrderTx 统一决定, 沙箱和真实回调走同一段逻辑。退款能释放托管中的任务,但不会去动已经结算 的那一笔——钱已经在喂养者账上,那种情况必须有人看过再说。 通道接通之前,客服可以在管理端手工标记已托管,必须写明钱怎么收的,进审计日志。 另外补上三件方案里写了但代码没做的事:打卡必须按到达、喂食、离开的顺序, 喂食离到达太近视为摆拍;地址新增紧急联系人与常去医院,随门牌一起只发给 被选中的喂养者;任务结束七天后由后台任务抹掉地址明文,只留城市与街道。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
72 lines
2.5 KiB
Go
72 lines
2.5 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"time"
|
|
)
|
|
|
|
// The address table holds a stranger's door number and phone. Keeping that
|
|
// after the job is over buys nothing and costs a lot if the database ever
|
|
// leaks, so a finished task schedules its address for erasure and this worker
|
|
// carries it out. The city and street stay: they are what the history reads
|
|
// from, and they were public on the listing anyway.
|
|
|
|
func (a *App) scheduleAddressPurge(ctx context.Context) int64 {
|
|
days := a.configInt(ctx, "pet.address_purge_days", 7)
|
|
if days < 1 {
|
|
days = 7
|
|
}
|
|
// 只给"没有任何进行中任务"的地址排期;主人常用的地址不会因为一次任务结束就被清空。
|
|
result, err := a.db.ExecContext(ctx, `UPDATE pet_addresses addr
|
|
JOIN (SELECT address_id,MAX(updated_at) AS finished FROM pet_feed_tasks
|
|
WHERE status IN ('SETTLED','REFUNDED','CANCELLED','ARBITRATED') GROUP BY address_id) ended
|
|
ON ended.address_id=addr.id
|
|
SET addr.purge_at=DATE_ADD(ended.finished,INTERVAL ? DAY)
|
|
WHERE addr.purge_at IS NULL
|
|
AND NOT EXISTS (SELECT 1 FROM pet_feed_tasks live WHERE live.address_id=addr.id
|
|
AND live.status IN ('CREATED','ESCROWED','ASSIGNED','SERVING','COMPLETED','DISPUTED'))`, days)
|
|
if err != nil {
|
|
log.Printf("pet address purge scheduling failed: %v", err)
|
|
return 0
|
|
}
|
|
affected, _ := result.RowsAffected()
|
|
return affected
|
|
}
|
|
|
|
func (a *App) purgeDueAddresses(ctx context.Context) int64 {
|
|
// 一个地址重新被用于新任务时会清掉 purge_at;这里再确认一次没有活任务,
|
|
// 免得排期和复用之间抢到一起。
|
|
result, err := a.db.ExecContext(ctx, `UPDATE pet_addresses addr
|
|
SET addr.detail_cipher='',addr.contact_cipher='',addr.emergency_cipher=NULL,addr.vet_hospital='',
|
|
addr.latitude=NULL,addr.longitude=NULL,addr.purge_at=NULL
|
|
WHERE addr.purge_at IS NOT NULL AND addr.purge_at<=NOW(3)
|
|
AND NOT EXISTS (SELECT 1 FROM pet_feed_tasks live WHERE live.address_id=addr.id
|
|
AND live.status IN ('CREATED','ESCROWED','ASSIGNED','SERVING','COMPLETED','DISPUTED'))`)
|
|
if err != nil {
|
|
log.Printf("pet address purge failed: %v", err)
|
|
return 0
|
|
}
|
|
affected, _ := result.RowsAffected()
|
|
if affected > 0 {
|
|
log.Printf("pet address purge: cleared plaintext for %d addresses", affected)
|
|
}
|
|
return affected
|
|
}
|
|
|
|
func (a *App) startAddressPurgeWorker(ctx context.Context) {
|
|
go func() {
|
|
ticker := time.NewTicker(time.Hour)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
a.scheduleAddressPurge(ctx)
|
|
a.purgeDueAddresses(ctx)
|
|
}
|
|
}
|
|
}()
|
|
}
|