package api import ( "database/sql" "encoding/json" "io" "log" "net/http" "strings" "time" "fmt" ) type TemplatesAPI struct { meta *sql.DB marketing *sql.DB } func TemplatesHandler(meta, marketing *sql.DB) http.Handler { api := &TemplatesAPI{meta: meta, marketing: marketing} return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { p := strings.TrimPrefix(r.URL.Path, "/api/templates") if r.Method == http.MethodPost && p == "" { api.createTemplate(w, r) return } if r.Method == http.MethodGet && p == "" { api.listTemplates(w, r) return } if strings.HasPrefix(p, "/") { id := strings.TrimPrefix(p, "/") if r.Method == http.MethodGet { api.getTemplate(w, r, id) return } if r.Method == http.MethodPatch { api.patchTemplate(w, r, id) return } if r.Method == http.MethodDelete { api.deleteTemplate(w, r, id) return } if r.Method == http.MethodPost && strings.HasSuffix(p, "/validate") { id = strings.TrimSuffix(id, "/validate") api.validateTemplate(w, r, id) return } } fail(w, r, http.StatusNotFound, "not found") }) } type TemplatePayload struct { Name string `json:"name"` Datasource string `json:"datasource"` MainTable string `json:"main_table"` Fields []string `json:"fields"` Filters map[string]interface{} `json:"filters"` FileFormat string `json:"file_format"` OwnerID uint64 `json:"owner_id"` Visibility string `json:"visibility"` } func (a *TemplatesAPI) createTemplate(w http.ResponseWriter, r *http.Request) { b, _ := io.ReadAll(r.Body) var p TemplatePayload json.Unmarshal(b, &p) r = WithPayload(r, p) uidStr := r.URL.Query().Get("userId") if uidStr != "" { var uid uint64 _, _ = fmt.Sscan(uidStr, &uid) if uid > 0 { p.OwnerID = uid } } now := time.Now() tplSQL := "INSERT INTO export_templates (name, datasource, main_table, fields_json, filters_json, file_format, visibility, owner_id, enabled, stats_enabled, last_validated_at, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)" tplArgs := []interface{}{p.Name, p.Datasource, p.MainTable, toJSON(p.Fields), toJSON(p.Filters), p.FileFormat, p.Visibility, p.OwnerID, 1, 0, now, now, now} log.Printf("trace_id=%s sql=%s args=%v", TraceIDFrom(r), tplSQL, tplArgs) _, err := a.meta.Exec(tplSQL, tplArgs...) if err != nil { fail(w, r, http.StatusInternalServerError, err.Error()) return } writeJSON(w, r, http.StatusCreated, 0, "ok", nil) } func (a *TemplatesAPI) listTemplates(w http.ResponseWriter, r *http.Request) { uidStr := r.URL.Query().Get("userId") sqlText := "SELECT id,name,datasource,main_table,file_format,visibility,owner_id,enabled,last_validated_at,created_at,updated_at, COALESCE(JSON_LENGTH(fields_json),0) AS field_count, (SELECT COUNT(1) FROM export_jobs ej WHERE ej.template_id = export_templates.id) AS exec_count FROM export_templates" args := []interface{}{} if uidStr != "" { sqlText += " WHERE owner_id IN (0, ?)" args = append(args, uidStr) } sqlText += " ORDER BY updated_at DESC LIMIT 200" rows, err := a.meta.Query(sqlText, args...) if err != nil { fail(w, r, http.StatusInternalServerError, err.Error()) return } defer rows.Close() out := []map[string]interface{}{} for rows.Next() { var id uint64 var name, datasource, mainTable, fileFormat, visibility string var ownerID uint64 var enabled int var lastValidatedAt sql.NullTime var createdAt, updatedAt time.Time var fieldCount, execCount int64 err := rows.Scan(&id, &name, &datasource, &mainTable, &fileFormat, &visibility, &ownerID, &enabled, &lastValidatedAt, &createdAt, &updatedAt, &fieldCount, &execCount) if err != nil { fail(w, r, http.StatusInternalServerError, err.Error()) return } m := map[string]interface{}{"id": id, "name": name, "datasource": datasource, "main_table": mainTable, "file_format": fileFormat, "visibility": visibility, "owner_id": ownerID, "enabled": enabled == 1, "last_validated_at": lastValidatedAt.Time, "created_at": createdAt, "updated_at": updatedAt, "field_count": fieldCount, "exec_count": execCount} out = append(out, m) } ok(w, r, out) } func (a *TemplatesAPI) getTemplate(w http.ResponseWriter, r *http.Request, id string) { row := a.meta.QueryRow("SELECT id,name,datasource,main_table,fields_json,filters_json,file_format,visibility,owner_id,enabled,explain_score,last_validated_at,created_at,updated_at FROM export_templates WHERE id=?", id) var m = map[string]interface{}{} var tid uint64 var name, datasource, mainTable, fileFormat, visibility string var ownerID uint64 var enabled int var explainScore sql.NullInt64 var lastValidatedAt sql.NullTime var createdAt, updatedAt time.Time var fields, filters []byte err := row.Scan(&tid, &name, &datasource, &mainTable, &fields, &filters, &fileFormat, &visibility, &ownerID, &enabled, &explainScore, &lastValidatedAt, &createdAt, &updatedAt) if err != nil { fail(w, r, http.StatusNotFound, "not found") return } m["id"] = tid m["name"] = name m["datasource"] = datasource m["main_table"] = mainTable m["file_format"] = fileFormat m["visibility"] = visibility m["owner_id"] = ownerID m["enabled"] = enabled == 1 m["explain_score"] = explainScore.Int64 m["last_validated_at"] = lastValidatedAt.Time m["created_at"] = createdAt m["updated_at"] = updatedAt m["fields"] = fromJSON(fields) m["filters"] = fromJSON(filters) ok(w, r, m) } func (a *TemplatesAPI) patchTemplate(w http.ResponseWriter, r *http.Request, id string) { b, _ := io.ReadAll(r.Body) var p map[string]interface{} json.Unmarshal(b, &p) set := []string{} args := []interface{}{} for k, v := range p { switch k { case "name", "visibility", "file_format": set = append(set, k+"=?") args = append(args, v) case "fields": set = append(set, "fields_json=?") args = append(args, toJSON(v)) case "filters": set = append(set, "filters_json=?") args = append(args, toJSON(v)) case "enabled": set = append(set, "enabled=?") if v.(bool) { args = append(args, 1) } else { args = append(args, 0) } } } if len(set) == 0 { fail(w, r, http.StatusBadRequest, "no patch") return } // ensure updated_at set = append(set, "updated_at=?") args = append(args, time.Now(), id) _, err := a.meta.Exec("UPDATE export_templates SET "+strings.Join(set, ",")+" WHERE id= ?", args...) if err != nil { fail(w, r, http.StatusInternalServerError, err.Error()) return } ok(w, r, nil) } func (a *TemplatesAPI) deleteTemplate(w http.ResponseWriter, r *http.Request, id string) { var cnt int64 row := a.meta.QueryRow("SELECT COUNT(1) FROM export_jobs WHERE template_id=?", id) _ = row.Scan(&cnt) if cnt > 0 { fail(w, r, http.StatusBadRequest, "template in use") return } _, err := a.meta.Exec("DELETE FROM export_templates WHERE id= ?", id) if err != nil { fail(w, r, http.StatusInternalServerError, err.Error()) return } ok(w, r, nil) } func (a *TemplatesAPI) validateTemplate(w http.ResponseWriter, r *http.Request, id string) { row := a.meta.QueryRow("SELECT main_table, fields_json, filters_json FROM export_templates WHERE id=?", id) var main string var fields, filters []byte err := row.Scan(&main, &fields, &filters) if err != nil { fail(w, r, http.StatusNotFound, "not found") return } var fs []string var fl map[string]interface{} json.Unmarshal(fields, &fs) json.Unmarshal(filters, &fl) ok(w, r, nil) } func toJSON(v interface{}) []byte { b, _ := json.Marshal(v) return b } func fromJSON(b []byte) interface{} { var v interface{} json.Unmarshal(b, &v) return v } func Whitelist() map[string]bool { m := map[string]bool{ "order.order_number": true, "order.key": true, "order.creator": true, "order.out_trade_no": true, "order.type": true, "order.status": true, "order.account": true, "order.product_id": true, "order.reseller_id": true, "order.plan_id": true, "order.key_batch_id": true, "order.code_batch_id": true, "order.pay_type": true, "order.pay_status": true, "order.use_coupon": true, "order.deliver_status": true, "order.expire_time": true, "order.recharge_time": true, "order.contract_price": true, "order.num": true, "order.total": true, "order.pay_amount": true, "order.create_time": true, "order.update_time": true, "order.official_price": true, "order.merchant_name": true, "order.activity_name": true, "order.goods_name": true, "order.pay_time": true, "order.coupon_id": true, "order.discount_amount": true, "order.supplier_product_name": true, "order.is_inner": true, "order.icon": true, "order.cost_price": true, "order.success_num": true, "order.is_reset": true, "order.is_retry": true, "order.channel": true, "order.is_store": true, "order.trace_id": true, "order.out_order_no": true, "order.next_retry_time": true, "order.recharge_suc_time": true, "order.supplier_id": true, "order.supplier_product_id": true, "order.merchant_id": true, "order.goods_id": true, "order.activity_id": true, "order.key_batch_name": true, "order_detail.plan_title": true, "order_detail.reseller_name": true, "order_detail.product_name": true, "order_detail.show_url": true, "order_detail.official_price": true, "order_detail.cost_price": true, "order_detail.create_time": true, "order_detail.update_time": true, "order_cash.channel": true, "order_cash.order_no": true, "order_cash.trade_no": true, "order_cash.wechat_detail_id": true, "order_cash.denomination": true, "order_cash.account": true, "order_cash.receive_name": true, "order_cash.app_id": true, "order_cash.cash_activity_id": true, "order_cash.receive_status": true, "order_cash.receive_time": true, "order_cash.success_time": true, "order_cash.cash_packet_id": true, "order_cash.cash_id": true, "order_cash.amount": true, "order_cash.activity_id": true, "order_cash.goods_id": true, "order_cash.merchant_id": true, "order_cash.supplier_id": true, "order_cash.user_id": true, "order_cash.status": true, "order_cash.expire_time": true, "order_cash.create_time": true, "order_cash.update_time": true, "order_cash.version": true, "order_cash.is_confirm": true, "order_voucher.channel": true, "order_voucher.channel_activity_id": true, "order_voucher.channel_voucher_id": true, "order_voucher.status": true, "order_voucher.receive_mode": true, "order_voucher.grant_time": true, "order_voucher.usage_time": true, "order_voucher.refund_time": true, "order_voucher.status_modify_time": true, "order_voucher.overdue_time": true, "order_voucher.refund_amount": true, "order_voucher.official_price": true, "order_voucher.out_biz_no": true, "order_voucher.account_no": true, "plan.id": true, "plan.title": true, "plan.status": true, "plan.begin_time": true, "plan.end_time": true, "key_batch.id": true, "key_batch.batch_name": true, "key_batch.bind_object": true, "key_batch.quantity": true, "key_batch.stock": true, "key_batch.begin_time": true, "key_batch.end_time": true, "code_batch.id": true, "code_batch.title": true, "code_batch.status": true, "code_batch.begin_time": true, "code_batch.end_time": true, "code_batch.quantity": true, "code_batch.usage": true, "code_batch.stock": true, "voucher.channel": true, "voucher.channel_activity_id": true, "voucher.price": true, "voucher.balance": true, "voucher.used_amount": true, "voucher.denomination": true, "voucher_batch.channel_activity_id": true, "voucher_batch.temp_no": true, "voucher_batch.provider": true, "voucher_batch.weight": true, "merchant_key_send.merchant_id": true, "merchant_key_send.out_biz_no": true, "merchant_key_send.key": true, "merchant_key_send.status": true, "merchant_key_send.usage_time": true, "merchant_key_send.create_time": true, "order_digit.order_no": true, "order_digit.card_no": true, "order_digit.account": true, "order_digit.goods_id": true, "order_digit.merchant_id": true, "order_digit.supplier_id": true, "order_digit.activity_id": true, "order_digit.user_id": true, "order_digit.success_time": true, "order_digit.supplier_product_no": true, "order_digit.order_type": true, "order_digit.end_time": true, "order_digit.create_time": true, "order_digit.update_time": true, "order_digit.code": true, "order_digit.sms_channel": true, "goods_voucher_batch.channel_batch_no": true, "goods_voucher_batch.voucher_subject_id": true, "goods_voucher_batch.id": true, "goods_voucher_batch.goods_voucher_id": true, "goods_voucher_batch.supplier_id": true, "goods_voucher_batch.temp_no": true, "goods_voucher_batch.index": true, "goods_voucher_batch.create_time": true, "goods_voucher_batch.update_time": true, "goods_voucher_subject_config.id": true, "goods_voucher_subject_config.name": true, "goods_voucher_subject_config.type": true, "goods_voucher_subject_config.create_time": true, "merchant.id": true, "merchant.name": true, "merchant.user_id": true, "merchant.merchant_no": true, "merchant.subject": true, "merchant.third_party": true, "merchant.status": true, "merchant.balance": true, "merchant.total_consumption": true, "merchant.contact_name": true, "merchant.contact_phone": true, "merchant.contact_email": true, "merchant.create_time": true, "merchant.update_time": true, "activity.id": true, "activity.name": true, "activity.user_id": true, "activity.merchant_id": true, "activity.user_name": true, "activity.activity_no": true, "activity.status": true, "activity.key_total_num": true, "activity.key_generate_num": true, "activity.key_usable_num": true, "activity.domain_url": true, "activity.theme_login_id": true, "activity.theme_list_id": true, "activity.theme_verify_id": true, "activity.settlement_type": true, "activity.key_expire_type": true, "activity.key_valid_day": true, "activity.key_begin_time": true, "activity.key_end_time": true, "activity.key_style": true, "activity.begin_time": true, "activity.end_time": true, "activity.is_retry": true, "activity.create_time": true, "activity.update_time": true, "activity.discard_time": true, "activity.delete_time": true, "activity.auto_charge": true, "activity.stock": true, "activity.approval_trade_no": true, "activity.amount": true, "activity.channels": true, "activity.key_begin": true, "activity.key_end": true, "activity.key_unit": true, "activity.key_pay_button_text": true, "activity.goods_pay_button_text": true, "activity.is_open_db_transaction": true, "activity.bank_tag": true, } return m } func FieldLabels() map[string]string { return map[string]string{ "order.order_number": "订单编号", "order.key": "KEY", "order.creator": "创建者ID", "order.out_trade_no": "支付流水号", "order.type": "订单类型", "order.status": "订单状态", "order.account": "账号", "order.product_id": "商品ID", "order.reseller_id": "分销商ID", "order.plan_id": "计划ID", "order.key_batch_id": "KEY批次ID", "order.code_batch_id": "兑换批次ID", "order.pay_type": "支付方式", "order.pay_status": "支付状态", "order.use_coupon": "是否使用优惠券", "order.deliver_status": "投递状态", "order.expire_time": "过期处理时间", "order.recharge_time": "充值时间", "order.contract_price": "合同单价", "order.num": "数量", "order.total": "总金额", "order.pay_amount": "支付金额", "order.create_time": "创建时间", "order.update_time": "更新时间", "order.official_price": "官方价", "order.merchant_name": "分销商名称", "order.activity_name": "活动名称", "order.goods_name": "商品名称", "order.pay_time": "支付时间", "order.coupon_id": "优惠券ID", "order.discount_amount": "优惠金额", "order.supplier_product_name": "供应商产品名称", "order.is_inner": "内部供应商订单", "order.icon": "订单图片", "order.cost_price": "成本价", "order.success_num": "到账数量", "order.is_reset": "是否重置", "order.is_retry": "是否重试", "order.channel": "支付渠道", "order.is_store": "是否退还库存", "order.trace_id": "TraceID", "order.out_order_no": "外部订单号", "order.next_retry_time": "下次重试时间", "order.recharge_suc_time": "充值成功时间", "order.supplier_id": "供应商ID", "order.supplier_product_id": "供应商产品ID", "order.merchant_id": "分销商ID", "order.goods_id": "商品ID", "order.activity_id": "活动ID", "order.key_batch_name": "key批次名称", "order_detail.plan_title": "计划标题", "order_detail.reseller_name": "分销商名称", "order_detail.product_name": "商品名称", "order_detail.show_url": "商品图片URL", "order_detail.official_price": "官方价", "order_detail.cost_price": "成本价", "order_detail.create_time": "创建时间", "order_detail.update_time": "更新时间", "order_cash.order_no": "订单号", "order_cash.trade_no": "交易号", "order_cash.wechat_detail_id": "微信明细单号", "order_cash.channel": "渠道", "order_cash.denomination": "红包面额", "order_cash.account": "领取账号", "order_cash.receive_name": "真实姓名", "order_cash.app_id": "转账AppID", "order_cash.cash_activity_id": "红包批次号", "order_cash.receive_status": "领取状态", "order_cash.receive_time": "拆红包时间", "order_cash.success_time": "成功时间", "order_cash.cash_packet_id": "红包ID", "order_cash.cash_id": "红包规则ID", "order_cash.amount": "红包额度", "order_cash.activity_id": "活动ID", "order_cash.goods_id": "商品ID", "order_cash.merchant_id": "分销商ID", "order_cash.supplier_id": "供应商ID", "order_cash.user_id": "创建者ID", "order_cash.status": "状态", "order_cash.expire_time": "过期时间", "order_cash.create_time": "创建时间", "order_cash.update_time": "更新时间", "order_cash.version": "版本", "order_cash.is_confirm": "是否确认", "order_voucher.channel": "渠道", "order_voucher.channel_activity_id": "渠道立减金批次", "order_voucher.channel_voucher_id": "渠道立减金ID", "order_voucher.status": "状态", "order_voucher.receive_mode": "领取方式", "order_voucher.grant_time": "领取时间", "order_voucher.usage_time": "核销时间", "order_voucher.refund_time": "退款时间", "order_voucher.status_modify_time": "状态更新时间", "order_voucher.overdue_time": "过期时间", "order_voucher.refund_amount": "退款金额", "order_voucher.official_price": "官方价", "order_voucher.out_biz_no": "外部业务号", "order_voucher.account_no": "账户号", "plan.id": "计划ID", "plan.title": "计划标题", "plan.status": "状态", "plan.begin_time": "开始时间", "plan.end_time": "结束时间", "key_batch.id": "批次ID", "key_batch.batch_name": "批次名称", "key_batch.bind_object": "绑定对象", "key_batch.quantity": "发放数量", "key_batch.stock": "剩余库存", "key_batch.begin_time": "开始时间", "key_batch.end_time": "结束时间", "code_batch.id": "兑换批次ID", "code_batch.title": "标题", "code_batch.status": "状态", "code_batch.begin_time": "开始时间", "code_batch.end_time": "结束时间", "code_batch.quantity": "数量", "code_batch.usage": "使用数", "code_batch.stock": "库存", "voucher.channel": "渠道", "voucher.channel_activity_id": "渠道批次号", "voucher.price": "合同单价", "voucher.balance": "剩余额度", "voucher.used_amount": "已用额度", "voucher.denomination": "面额", "voucher_batch.channel_activity_id": "渠道批次号", "voucher_batch.temp_no": "模板编号", "voucher_batch.provider": "服务商", "voucher_batch.weight": "权重", "merchant_key_send.merchant_id": "商户ID", "merchant_key_send.out_biz_no": "商户业务号", "merchant_key_send.key": "券码", "merchant_key_send.status": "状态", "merchant_key_send.usage_time": "核销时间", "merchant_key_send.create_time": "创建时间", "order_digit.order_no": "订单号", "order_digit.card_no": "卡号", "order_digit.account": "充值账号", "order_digit.goods_id": "商品ID", "order_digit.merchant_id": "分销商ID", "order_digit.supplier_id": "供应商ID", "order_digit.activity_id": "活动ID", "order_digit.user_id": "创建者ID", "order_digit.success_time": "到账时间", "order_digit.supplier_product_no": "供应商产品编码", "order_digit.order_type": "订单类型", "order_digit.end_time": "卡密有效期", "order_digit.create_time": "创建时间", "order_digit.update_time": "更新时间", "order_digit.code": "验证码", "order_digit.sms_channel": "短信渠道", "goods_voucher_batch.channel_batch_no": "渠道批次号", "goods_voucher_batch.voucher_subject_id": "主体配置ID", "goods_voucher_batch.id": "ID", "goods_voucher_batch.goods_voucher_id": "立减金ID", "goods_voucher_batch.supplier_id": "供应商ID", "goods_voucher_batch.temp_no": "模板编号", "goods_voucher_batch.index": "权重", "goods_voucher_batch.create_time": "创建时间", "goods_voucher_batch.update_time": "更新时间", "goods_voucher_subject_config.id": "主体配置ID", "goods_voucher_subject_config.name": "主体名称", "goods_voucher_subject_config.type": "主体类型", "goods_voucher_subject_config.create_time": "创建时间", "merchant.id": "客户ID", "merchant.name": "客户名称", "merchant.user_id": "用户中心ID", "merchant.merchant_no": "商户编码", "merchant.subject": "客户主体", "merchant.third_party": "来源类型", "merchant.status": "状态", "merchant.balance": "客户余额", "merchant.total_consumption": "累计消费", "merchant.contact_name": "联系人名称", "merchant.contact_phone": "联系人电话", "merchant.contact_email": "联系人Email", "merchant.create_time": "创建时间", "merchant.update_time": "编辑时间", "activity.id": "活动ID", "activity.name": "活动名称", "activity.user_id": "创建者ID", "activity.merchant_id": "客户ID", "activity.user_name": "创建者名称", "activity.activity_no": "活动编号", "activity.status": "状态", "activity.key_total_num": "Key码总量", "activity.key_generate_num": "Key码已生成数量", "activity.key_usable_num": "Key可使用次数", "activity.domain_url": "域名", "activity.theme_login_id": "登录模版ID", "activity.theme_list_id": "列表模版ID", "activity.theme_verify_id": "验证模版ID", "activity.settlement_type": "结算方式", "activity.key_expire_type": "Key有效期类型", "activity.key_valid_day": "有效天数", "activity.key_begin_time": "Key有效开始时间", "activity.key_end_time": "Key有效结束时间", "activity.key_style": "Key样式", "activity.begin_time": "开始时间", "activity.end_time": "结束时间", "activity.is_retry": "是否自动重试", "activity.create_time": "创建时间", "activity.update_time": "修改时间", "activity.discard_time": "作废时间", "activity.delete_time": "删除时间", "activity.auto_charge": "是否充值到账", "activity.stock": "已使用库存", "activity.approval_trade_no": "审批交易号", "activity.amount": "支付金额", "activity.channels": "支付渠道", "activity.key_begin": "开始月份", "activity.key_end": "截止月份", "activity.key_unit": "时间单位", "activity.key_pay_button_text": "Key支付按钮文本", "activity.goods_pay_button_text": "商品支付按钮文本", "activity.is_open_db_transaction": "是否开启事务", "activity.bank_tag": "银行标识", } }