466 lines
17 KiB
Go
466 lines
17 KiB
Go
|
|
package logic
|
|||
|
|
|
|||
|
|
import (
|
|||
|
|
"context"
|
|||
|
|
"fmt"
|
|||
|
|
"strconv"
|
|||
|
|
"strings"
|
|||
|
|
|
|||
|
|
"github.com/gogf/gf/v2/database/gdb"
|
|||
|
|
"github.com/gogf/gf/v2/errors/gerror"
|
|||
|
|
"github.com/gogf/gf/v2/frame/g"
|
|||
|
|
"github.com/gogf/gf/v2/os/gtime"
|
|||
|
|
|
|||
|
|
adminv1 "tool-api/api/admin/v1"
|
|||
|
|
"tool-api/internal/consts"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
// ============================================================================
|
|||
|
|
// 开发项目四期:维护模板 / 付款分期模板 / 分期到账 / 维护期 / 每日提醒扫描
|
|||
|
|
//(迭代-2026-09-30,交付流程:需求提交 → 功能确认 → 支付定金 → 开发 → 验收 → 维护期)
|
|||
|
|
// ============================================================================
|
|||
|
|
|
|||
|
|
// ---------- 维护模板 ----------
|
|||
|
|
|
|||
|
|
// AdminMaintainPlanList 维护模板列表(含停用)
|
|||
|
|
func AdminMaintainPlanList(ctx context.Context) (*adminv1.AdminMaintainPlanListRes, error) {
|
|||
|
|
records, err := g.Model(consts.TableMaintainPlans).Order("sort asc, id asc").All()
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
list := make([]adminv1.AdminMaintainPlanItem, 0, len(records))
|
|||
|
|
for _, r := range records {
|
|||
|
|
list = append(list, adminv1.AdminMaintainPlanItem{
|
|||
|
|
Id: r["id"].Int64(),
|
|||
|
|
Name: r["name"].String(),
|
|||
|
|
FreeYears: r["free_years"].Int(),
|
|||
|
|
YearlyFeeCents: r["yearly_fee_cents"].Int64(),
|
|||
|
|
SourceOnly: r["source_only"].Int(),
|
|||
|
|
Sort: r["sort"].Int(),
|
|||
|
|
IsEnabled: r["is_enabled"].Int(),
|
|||
|
|
Remark: r["remark"].String(),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
return &adminv1.AdminMaintainPlanListRes{List: list}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AdminMaintainPlanSave 保存维护模板。仅源码模板不涉及免费年限与年费(统一清零)。
|
|||
|
|
func AdminMaintainPlanSave(ctx context.Context, req *adminv1.AdminMaintainPlanSaveReq) (*adminv1.AdminMaintainPlanSaveRes, error) {
|
|||
|
|
name := strings.TrimSpace(req.Name)
|
|||
|
|
if name == "" {
|
|||
|
|
return nil, gerror.New("请填写模板名称")
|
|||
|
|
}
|
|||
|
|
if req.SourceOnly == 1 {
|
|||
|
|
req.YearlyFeeCents = 0
|
|||
|
|
req.FreeYears = 0
|
|||
|
|
}
|
|||
|
|
data := g.Map{
|
|||
|
|
"name": name,
|
|||
|
|
"free_years": req.FreeYears,
|
|||
|
|
"yearly_fee_cents": req.YearlyFeeCents,
|
|||
|
|
"source_only": req.SourceOnly,
|
|||
|
|
"sort": req.Sort,
|
|||
|
|
"is_enabled": req.IsEnabled,
|
|||
|
|
"remark": strings.TrimSpace(req.Remark),
|
|||
|
|
}
|
|||
|
|
if req.Id > 0 {
|
|||
|
|
data["updated_at"] = gdb.Raw("NOW()")
|
|||
|
|
if _, err := g.Model(consts.TableMaintainPlans).Where("id", req.Id).Data(data).Update(); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
return &adminv1.AdminMaintainPlanSaveRes{Id: req.Id}, nil
|
|||
|
|
}
|
|||
|
|
data["created_at"] = gdb.Raw("NOW()")
|
|||
|
|
data["updated_at"] = gdb.Raw("NOW()")
|
|||
|
|
res, err := g.Model(consts.TableMaintainPlans).Data(data).Insert()
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
id, _ := res.LastInsertId()
|
|||
|
|
return &adminv1.AdminMaintainPlanSaveRes{Id: id}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AdminMaintainPlanDelete 删除维护模板(项目上的历史引用展示为「模板已删」)。
|
|||
|
|
func AdminMaintainPlanDelete(ctx context.Context, id int64) (*adminv1.AdminMaintainPlanDeleteRes, error) {
|
|||
|
|
if _, err := g.Model(consts.TableMaintainPlans).Where("id", id).Delete(); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
return &adminv1.AdminMaintainPlanDeleteRes{}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 付款分期模板 ----------
|
|||
|
|
|
|||
|
|
// AdminPayPlanList 分期模板列表(含停用)
|
|||
|
|
func AdminPayPlanList(ctx context.Context) (*adminv1.AdminPayPlanListRes, error) {
|
|||
|
|
records, err := g.Model(consts.TablePayPlans).Order("sort asc, id asc").All()
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
list := make([]adminv1.AdminPayPlanItem, 0, len(records))
|
|||
|
|
for _, r := range records {
|
|||
|
|
list = append(list, adminv1.AdminPayPlanItem{
|
|||
|
|
Id: r["id"].Int64(),
|
|||
|
|
Name: r["name"].String(),
|
|||
|
|
Schedule: r["schedule"].String(),
|
|||
|
|
Sort: r["sort"].Int(),
|
|||
|
|
IsEnabled: r["is_enabled"].Int(),
|
|||
|
|
Remark: r["remark"].String(),
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
return &adminv1.AdminPayPlanListRes{List: list}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AdminPayPlanSave 保存分期模板(schedule JSON 校验:比例合计 100、触发节点合法)。
|
|||
|
|
func AdminPayPlanSave(ctx context.Context, req *adminv1.AdminPayPlanSaveReq) (*adminv1.AdminPayPlanSaveRes, error) {
|
|||
|
|
name := strings.TrimSpace(req.Name)
|
|||
|
|
if name == "" {
|
|||
|
|
return nil, gerror.New("请填写模板名称")
|
|||
|
|
}
|
|||
|
|
if _, err := parsePaySchedule(req.Schedule); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
data := g.Map{
|
|||
|
|
"name": name,
|
|||
|
|
"schedule": strings.TrimSpace(req.Schedule),
|
|||
|
|
"sort": req.Sort,
|
|||
|
|
"is_enabled": req.IsEnabled,
|
|||
|
|
"remark": strings.TrimSpace(req.Remark),
|
|||
|
|
}
|
|||
|
|
if req.Id > 0 {
|
|||
|
|
data["updated_at"] = gdb.Raw("NOW()")
|
|||
|
|
if _, err := g.Model(consts.TablePayPlans).Where("id", req.Id).Data(data).Update(); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
return &adminv1.AdminPayPlanSaveRes{Id: req.Id}, nil
|
|||
|
|
}
|
|||
|
|
data["created_at"] = gdb.Raw("NOW()")
|
|||
|
|
data["updated_at"] = gdb.Raw("NOW()")
|
|||
|
|
res, err := g.Model(consts.TablePayPlans).Data(data).Insert()
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
id, _ := res.LastInsertId()
|
|||
|
|
return &adminv1.AdminPayPlanSaveRes{Id: id}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AdminPayPlanDelete 删除分期模板。
|
|||
|
|
func AdminPayPlanDelete(ctx context.Context, id int64) (*adminv1.AdminPayPlanDeleteRes, error) {
|
|||
|
|
if _, err := g.Model(consts.TablePayPlans).Where("id", id).Delete(); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
return &adminv1.AdminPayPlanDeleteRes{}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 分期到账 / 维护期 ----------
|
|||
|
|
|
|||
|
|
// AdminProjectPayMark 标记分期到账并通知用户。
|
|||
|
|
func AdminProjectPayMark(ctx context.Context, req *adminv1.AdminProjectPayMarkReq) (*adminv1.AdminProjectPayMarkRes, error) {
|
|||
|
|
pay, err := g.Model(consts.TableProjectPays).Where("id", req.Id).One()
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if pay.IsEmpty() {
|
|||
|
|
return nil, gerror.New("分期不存在")
|
|||
|
|
}
|
|||
|
|
if pay["status"].Int() == consts.PayStatusPaid {
|
|||
|
|
return nil, gerror.New("该分期已是到账状态")
|
|||
|
|
}
|
|||
|
|
p, err := g.Model(consts.TableServiceProjects).Where("id", pay["project_id"].Int64()).One()
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if _, err = g.Model(consts.TableProjectPays).Where("id", req.Id).Data(g.Map{
|
|||
|
|
"status": consts.PayStatusPaid,
|
|||
|
|
"paid_at": gdb.Raw("NOW()"),
|
|||
|
|
"paid_note": strings.TrimSpace(req.PaidNote),
|
|||
|
|
"updated_at": gdb.Raw("NOW()"),
|
|||
|
|
}).Update(); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
pushProjectUpdate(ctx, p["user_id"].Int64(), pay["project_id"].Int64(),
|
|||
|
|
"付款已确认",
|
|||
|
|
fmt.Sprintf("项目「%s」第 %d 期(¥%s,%s)已确认到账。",
|
|||
|
|
p["title"].String(), pay["seq"].Int(), centsYuanText(pay["amount_cents"].Int64()),
|
|||
|
|
consts.PayTriggerNameOf(pay["trigger"].String())))
|
|||
|
|
WriteAudit(ctx, AuditEntry{
|
|||
|
|
Action: "service.project.pay.mark",
|
|||
|
|
TargetType: "service_project_pay",
|
|||
|
|
TargetId: auditId(req.Id),
|
|||
|
|
After: g.Map{"seq": pay["seq"].Int(), "amount_cents": pay["amount_cents"].Int64()},
|
|||
|
|
Result: consts.AuditResultSuccess,
|
|||
|
|
})
|
|||
|
|
return &adminv1.AdminProjectPayMarkRes{}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AdminProjectPayUnmark 撤销到账(误操作回退)。
|
|||
|
|
func AdminProjectPayUnmark(ctx context.Context, id int64) (*adminv1.AdminProjectPayUnmarkRes, error) {
|
|||
|
|
if _, err := g.Model(consts.TableProjectPays).Where("id", id).Data(g.Map{
|
|||
|
|
"status": consts.PayStatusPending,
|
|||
|
|
"paid_at": nil,
|
|||
|
|
"paid_note": "",
|
|||
|
|
"updated_at": gdb.Raw("NOW()"),
|
|||
|
|
}).Update(); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
return &adminv1.AdminProjectPayUnmarkRes{}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AdminProjectMaintainRenew 维护费续收一年:已缴至顺延一年(从 max(已缴至, 今天) 起算)并通知用户。
|
|||
|
|
func AdminProjectMaintainRenew(ctx context.Context, id int64) (*adminv1.AdminProjectMaintainRenewRes, error) {
|
|||
|
|
p, err := g.Model(consts.TableServiceProjects).Where("id", id).One()
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if p.IsEmpty() {
|
|||
|
|
return nil, gerror.New("项目不存在")
|
|||
|
|
}
|
|||
|
|
if p["accepted"].Int() != 1 {
|
|||
|
|
return nil, gerror.New("项目尚未验收,无维护期可续")
|
|||
|
|
}
|
|||
|
|
plan, err := g.Model(consts.TableMaintainPlans).Where("id", p["maintain_plan_id"].Int64()).One()
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if plan.IsEmpty() || plan["source_only"].Int() == 1 || plan["yearly_fee_cents"].Int64() <= 0 {
|
|||
|
|
return nil, gerror.New("该项目的维护模板不涉及年度维护费")
|
|||
|
|
}
|
|||
|
|
base := p["maintain_paid_until"].GTime()
|
|||
|
|
now := gtime.Now()
|
|||
|
|
if base == nil || base.Before(now) {
|
|||
|
|
base = now
|
|||
|
|
}
|
|||
|
|
paidUntil := base.AddDate(1, 0, 0)
|
|||
|
|
if _, err = g.Model(consts.TableServiceProjects).Where("id", id).Data(g.Map{
|
|||
|
|
"maintain_paid_until": paidUntil,
|
|||
|
|
"last_maintain_remind_at": nil,
|
|||
|
|
"updated_at": gdb.Raw("NOW()"),
|
|||
|
|
}).Update(); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
pushProjectUpdate(ctx, p["user_id"].Int64(), id,
|
|||
|
|
"维护费已续",
|
|||
|
|
fmt.Sprintf("项目「%s」的维护服务已续费一年(¥%s/年),有效期至 %s。",
|
|||
|
|
p["title"].String(), centsYuanText(plan["yearly_fee_cents"].Int64()), paidUntil.Format("Y-m-d")))
|
|||
|
|
WriteAudit(ctx, AuditEntry{
|
|||
|
|
Action: "service.project.maintain.renew",
|
|||
|
|
TargetType: "service_project",
|
|||
|
|
TargetId: auditId(id),
|
|||
|
|
After: g.Map{"paid_until": paidUntil.String()},
|
|||
|
|
Result: consts.AuditResultSuccess,
|
|||
|
|
})
|
|||
|
|
return &adminv1.AdminProjectMaintainRenewRes{PaidUntil: paidUntil.Format("Y-m-d")}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AdminProjectMaintainAdjust 调整维护期起算日(重算已缴至:起点 + 免费年数)。
|
|||
|
|
func AdminProjectMaintainAdjust(ctx context.Context, req *adminv1.AdminProjectMaintainAdjustReq) (*adminv1.AdminProjectMaintainAdjustRes, error) {
|
|||
|
|
p, err := g.Model(consts.TableServiceProjects).Where("id", req.Id).One()
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
if p.IsEmpty() {
|
|||
|
|
return nil, gerror.New("项目不存在")
|
|||
|
|
}
|
|||
|
|
startAt, err := gtime.StrToTime(req.StartAt + " 00:00:00")
|
|||
|
|
if err != nil {
|
|||
|
|
return nil, gerror.New("起算日期格式不合法")
|
|||
|
|
}
|
|||
|
|
freeYears := 0
|
|||
|
|
sourceOnly := 0
|
|||
|
|
if p["maintain_plan_id"].Int64() > 0 {
|
|||
|
|
plan, err := g.Model(consts.TableMaintainPlans).Where("id", p["maintain_plan_id"].Int64()).One()
|
|||
|
|
if err == nil && !plan.IsEmpty() {
|
|||
|
|
freeYears = plan["free_years"].Int()
|
|||
|
|
sourceOnly = plan["source_only"].Int()
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
var paidUntil *gtime.Time
|
|||
|
|
if sourceOnly != 1 && p["accepted"].Int() == 1 {
|
|||
|
|
paidUntil = startAt.AddDate(freeYears, 0, 0)
|
|||
|
|
}
|
|||
|
|
if _, err = g.Model(consts.TableServiceProjects).Where("id", req.Id).Data(g.Map{
|
|||
|
|
"maintain_start_at": startAt,
|
|||
|
|
"maintain_paid_until": paidUntil,
|
|||
|
|
"last_maintain_remind_at": nil,
|
|||
|
|
"updated_at": gdb.Raw("NOW()"),
|
|||
|
|
}).Update(); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
WriteAudit(ctx, AuditEntry{
|
|||
|
|
Action: "service.project.maintain.adjust",
|
|||
|
|
TargetType: "service_project",
|
|||
|
|
TargetId: auditId(req.Id),
|
|||
|
|
After: g.Map{"start_at": startAt.String(), "paid_until": paidUntil},
|
|||
|
|
Result: consts.AuditResultSuccess,
|
|||
|
|
})
|
|||
|
|
res := &adminv1.AdminProjectMaintainAdjustRes{}
|
|||
|
|
if paidUntil != nil {
|
|||
|
|
res.PaidUntil = paidUntil.Format("Y-m-d")
|
|||
|
|
}
|
|||
|
|
return res, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ---------- 提醒设置 ----------
|
|||
|
|
|
|||
|
|
// AdminMaintainRemindGet 提醒设置读取(settings)。
|
|||
|
|
func AdminMaintainRemindGet(ctx context.Context) (*adminv1.AdminMaintainRemindGetRes, error) {
|
|||
|
|
days, _ := g.Model(consts.TableSettings).Where("k", "service.maintain.remindDays").One()
|
|||
|
|
notify, _ := g.Model(consts.TableSettings).Where("k", "service.maintain.adminNotifyUserId").One()
|
|||
|
|
res := &adminv1.AdminMaintainRemindGetRes{RemindDays: 15}
|
|||
|
|
if !days.IsEmpty() {
|
|||
|
|
if v := days["v"].Int(); v > 0 {
|
|||
|
|
res.RemindDays = v
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
if !notify.IsEmpty() {
|
|||
|
|
res.AdminNotifyUserId = notify["v"].Int64()
|
|||
|
|
}
|
|||
|
|
return res, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// AdminMaintainRemindSave 提醒设置保存(remindDays / adminNotifyUserId)。
|
|||
|
|
func AdminMaintainRemindSave(ctx context.Context, req *adminv1.AdminMaintainRemindSaveReq) (*adminv1.AdminMaintainRemindSaveRes, error) {
|
|||
|
|
for _, kv := range []struct{ k, v string }{
|
|||
|
|
{"service.maintain.remindDays", strconv.Itoa(req.RemindDays)},
|
|||
|
|
{"service.maintain.adminNotifyUserId", strconv.FormatInt(req.AdminNotifyUserId, 10)},
|
|||
|
|
} {
|
|||
|
|
if err := settingUpsert(ctx, kv.k, kv.v); err != nil {
|
|||
|
|
return nil, err
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return &adminv1.AdminMaintainRemindSaveRes{}, nil
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// settingUpsert settings 单键写入(存在则更新,缺则补)。
|
|||
|
|
func settingUpsert(ctx context.Context, k, v string) error {
|
|||
|
|
cnt, err := g.Model(consts.TableSettings).Where("k", k).Count()
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
if cnt > 0 {
|
|||
|
|
_, err = g.Model(consts.TableSettings).Where("k", k).Data(g.Map{
|
|||
|
|
"v": v,
|
|||
|
|
"updated_at": gdb.Raw("NOW()"),
|
|||
|
|
}).Update()
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
_, err = g.Model(consts.TableSettings).Data(g.Map{
|
|||
|
|
"k": k,
|
|||
|
|
"v": v,
|
|||
|
|
"updated_at": gdb.Raw("NOW()"),
|
|||
|
|
}).Insert()
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ============================================================================
|
|||
|
|
// 维护费到期提醒:每天扫描(cmd.go 注册 cron)
|
|||
|
|
// - 范围:已验收 + 维护模板有年费(非仅源码)+ 已缴至距今天数 ≤ remindDays(含已逾期)
|
|||
|
|
// - 节奏:剩余 ≤3 天(含逾期)每天发;否则隔 3 天发一次(last_maintain_remind_at 判定)
|
|||
|
|
// - 通知:用户(站内信 + 订阅消息);超管(settings adminNotifyUserId 指定的小程序账号,
|
|||
|
|
// 同样站内信 + 订阅消息;未配置则只打日志——管理端开发项目页的待缴费横幅常驻可见)
|
|||
|
|
// ============================================================================
|
|||
|
|
|
|||
|
|
// ScanMaintainReminders 每日维护费提醒扫描。
|
|||
|
|
func ScanMaintainReminders(ctx context.Context) {
|
|||
|
|
remindDays, _ := g.Model(consts.TableSettings).Where("k", "service.maintain.remindDays").One()
|
|||
|
|
window := 15
|
|||
|
|
if !remindDays.IsEmpty() {
|
|||
|
|
if v := remindDays["v"].Int(); v > 0 {
|
|||
|
|
window = v
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
adminNotify, _ := g.Model(consts.TableSettings).Where("k", "service.maintain.adminNotifyUserId").One()
|
|||
|
|
adminUserId := int64(0)
|
|||
|
|
if !adminNotify.IsEmpty() {
|
|||
|
|
adminUserId = adminNotify["v"].Int64()
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
records, err := g.Model(consts.TableServiceProjects+" p").
|
|||
|
|
LeftJoin(consts.TableMaintainPlans+" mp", "mp.id=p.maintain_plan_id").
|
|||
|
|
Where("p.accepted", 1).
|
|||
|
|
Where("p.status", consts.ProjectStatusDone).
|
|||
|
|
Where("mp.source_only", 0).
|
|||
|
|
Where("mp.yearly_fee_cents > 0").
|
|||
|
|
Where("p.maintain_paid_until IS NOT NULL").
|
|||
|
|
Fields("p.*, mp.name as plan_name, mp.yearly_fee_cents").
|
|||
|
|
All()
|
|||
|
|
if err != nil {
|
|||
|
|
g.Log().Warningf(ctx, "[maintain-remind] 扫描失败: %v", err)
|
|||
|
|
return
|
|||
|
|
}
|
|||
|
|
now := gtime.Now()
|
|||
|
|
for _, r := range records {
|
|||
|
|
paidUntil := r["maintain_paid_until"].GTime()
|
|||
|
|
if paidUntil == nil {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
daysLeft := maintainDaysLeft(now, paidUntil)
|
|||
|
|
if daysLeft > window {
|
|||
|
|
continue
|
|||
|
|
}
|
|||
|
|
last := r["last_maintain_remind_at"].GTime()
|
|||
|
|
if daysLeft > 3 && last != nil && now.Sub(last).Hours() < 72 {
|
|||
|
|
continue // 未到最后 3 天时,隔 3 天发一次
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
title := "维护费缴纳提醒"
|
|||
|
|
content := fmt.Sprintf("项目「%s」的维护服务将于 %s 到期(剩余 %d 天),年度维护费 ¥%s,请及时缴纳。",
|
|||
|
|
r["title"].String(), paidUntil.Format("Y-m-d"), daysLeft, centsYuanText(r["yearly_fee_cents"].Int64()))
|
|||
|
|
if daysLeft < 0 {
|
|||
|
|
title = "维护费已到期"
|
|||
|
|
content = fmt.Sprintf("项目「%s」的维护服务已于 %s 到期,为不影响售后服务,请尽快缴纳年度维护费 ¥%s。",
|
|||
|
|
r["title"].String(), paidUntil.Format("Y-m-d"), centsYuanText(r["yearly_fee_cents"].Int64()))
|
|||
|
|
}
|
|||
|
|
link := fmt.Sprintf("/pkg-service/project-detail?id=%d", r["id"].Int64())
|
|||
|
|
userId := r["user_id"].Int64()
|
|||
|
|
PushMessage(ctx, userId, consts.MsgTypeProjectUpdate, title, content, link)
|
|||
|
|
|
|||
|
|
// 订阅消息(用户授权过才真正送达;模板未配置自动降级为站内信)
|
|||
|
|
if openid := userOpenid(ctx, userId); openid != "" {
|
|||
|
|
_ = SendProjectRemind(ctx, ProjectRemindSendReq{
|
|||
|
|
Openid: openid, Title: title, Content: content, Page: link,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
// 超管通知(settings 指定的小程序账号)
|
|||
|
|
if adminUserId > 0 && adminUserId != userId {
|
|||
|
|
PushMessage(ctx, adminUserId, consts.MsgTypeProjectUpdate, "【待缴费】"+title, content, link)
|
|||
|
|
if openid := userOpenid(ctx, adminUserId); openid != "" {
|
|||
|
|
_ = SendProjectRemind(ctx, ProjectRemindSendReq{
|
|||
|
|
Openid: openid, Title: title, Content: content, Page: link,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if _, err = g.Model(consts.TableServiceProjects).Where("id", r["id"].Int64()).Data(g.Map{
|
|||
|
|
"last_maintain_remind_at": gdb.Raw("NOW()"),
|
|||
|
|
}).Update(); err != nil {
|
|||
|
|
g.Log().Warningf(ctx, "[maintain-remind] 记录提醒时间失败 projectId=%d: %v", r["id"].Int64(), err)
|
|||
|
|
}
|
|||
|
|
g.Log().Infof(ctx, "[maintain-remind] 已提醒 projectId=%d daysLeft=%d", r["id"].Int64(), daysLeft)
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// maintainDaysLeft 距维护费到期天数(向上取整;已逾期为负数)。
|
|||
|
|
func maintainDaysLeft(now, paidUntil *gtime.Time) int {
|
|||
|
|
hours := paidUntil.Sub(now).Hours()
|
|||
|
|
if hours <= 0 {
|
|||
|
|
return -int(-hours/24) - 1
|
|||
|
|
}
|
|||
|
|
d := int(hours / 24)
|
|||
|
|
if hours-float64(d*24) > 0 {
|
|||
|
|
d++
|
|||
|
|
}
|
|||
|
|
return d
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// userOpenid 查用户 openid(站内信收件人对应的订阅消息送达对象)。
|
|||
|
|
func userOpenid(ctx context.Context, userId int64) string {
|
|||
|
|
if userId <= 0 {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
u, err := g.Model(consts.TableUsers).Where("id", userId).One()
|
|||
|
|
if err != nil || u.IsEmpty() {
|
|||
|
|
return ""
|
|||
|
|
}
|
|||
|
|
return u["openid"].String()
|
|||
|
|
}
|