diff --git a/application/api/controller/Country.php b/application/api/controller/Country.php index 39804e0..9f3f5fd 100644 --- a/application/api/controller/Country.php +++ b/application/api/controller/Country.php @@ -29,21 +29,28 @@ class Country extends Base $page = max(1, intval($this->request->param('page', 1))); $perPage = max(1, min(intval($this->request->param('per_page', 20)), 100)); - $query = Db::name('country'); - + $where = []; if ($keyword !== '') { - $query->where('zh_name|en_name|code', 'like', '%' . $keyword . '%'); + $where[] = ['zh_name|en_name|code', 'like', '%' . $keyword . '%']; } if ($isHot !== '' && $isHot !== '-1') { - $query->where('is_hot', intval($isHot)); + $where[] = ['is_hot', '=', intval($isHot)]; } if ($partition !== '' && $partition !== '-1') { - $query->where('partition', intval($partition)); + $where[] = ['partition', '=', intval($partition)]; } - $total = (clone $query)->count(); + $countQuery = Db::name('country'); + foreach ($where as $w) { + $countQuery->where($w[0], $w[1], $w[2]); + } + $total = $countQuery->count(); - $list = $query + $listQuery = Db::name('country'); + foreach ($where as $w) { + $listQuery->where($w[0], $w[1], $w[2]); + } + $list = $listQuery ->order('is_hot desc, zh_name asc') ->page($page, $perPage) ->select(); diff --git a/application/api/controller/EmailClient.php b/application/api/controller/EmailClient.php index aebf240..e771ed7 100644 --- a/application/api/controller/EmailClient.php +++ b/application/api/controller/EmailClient.php @@ -1381,25 +1381,26 @@ class EmailClient extends Base $taskName = trim($this->request->param('task_name', '')); $expertIds = trim($this->request->param('expert_ids', '')); $field = trim($this->request->param('field', '')); - $majorId = intval($this->request->param('major_id', 0)); + $fetchIds = trim($this->request->param('fetch_ids', '')); $smtpIds = trim($this->request->param('smtp_ids', '')); $minInterval = intval($this->request->param('min_interval', 30)); $maxInterval = intval($this->request->param('max_interval', 60)); $maxBounceRate = intval($this->request->param('max_bounce_rate', 5)); $noRepeatDays = intval($this->request->param('no_repeat_days', 7)); - $sendStartHour = intval($this->request->param('send_start_hour', 8)); - $sendEndHour = intval($this->request->param('send_end_hour', 22)); - $sendDate = trim($this->request->param('send_date', date("Y-m-d",strtotime('+1 day')))); + $sendStartHour = intval($this->request->param('send_start_hour', 8)); + $sendEndHour = intval($this->request->param('send_end_hour', 22)); + $sendDate = trim($this->request->param('send_date', date("Y-m-d",strtotime('+1 day')))); + $targetPartitions = trim($this->request->param('target_partitions', '')); + $targetCountryIds = trim($this->request->param('target_country_ids', '')); + $type = intval($this->request->param('type', 0)); + $expertType = intval($this->request->param('expert_type', 5)); - if (!$journalId || !$templateId) { - return jsonError('journal_id and template_id are required'); + if (!$journalId) { + return jsonError('journal_id is required'); } $journal_info = $this->journal_obj->where("journal_id",$journalId)->find(); -// if (empty($expertIds) && empty($field)) { -// return jsonError('expert_ids or field is required'); -// } $templateId = ($templateId == 0) ? intval($journal_info['default_template_id']) : $templateId; - $styleId = ($styleId == 0) ? intval($journal_info['default_style_id']) : $styleId; + $styleId = ($styleId == 0) ? intval(isset($journal_info['default_style_id']) ? $journal_info['default_style_id'] : 0) : $styleId; if($templateId==0){ return jsonError("template is not set!"); } @@ -1418,60 +1419,23 @@ class EmailClient extends Base $experts = []; if (!empty($expertIds)) { - // 显式点名的专家,只按 state 过滤,ltime 由外部自行控制 $ids = array_map('intval', explode(',', $expertIds)); $experts = Db::name('expert') ->where('expert_id', 'in', $ids) ->where('state', 0) ->select(); } else { - // 不传 expert_ids:根据期刊选定的推广领域(journal_promotion_field → expert_fetch)自动选择专家 - $journal = Db::name('journal')->where('journal_id', $journalId)->find(); - if (!$journal) { - return jsonError('Journal not found'); - } - - $promotionFields = Db::name('journal_promotion_field') - ->alias('jpf') - ->join('t_expert_fetch ef_fetch', 'ef_fetch.expert_fetch_id = jpf.expert_fetch_id', 'inner') - ->where('jpf.journal_id', $journalId) - ->where('jpf.state', 0) - ->where('ef_fetch.state', 0) - ->column('ef_fetch.field'); - $promotionFields = array_unique(array_filter(array_map('trim', $promotionFields))); - - $query = Db::name('expert')->alias('e') - ->join('t_expert_field ef', 'e.expert_id = ef.expert_id', 'inner') - ->where('e.state', 0) - ->where('ef.state', 0); - - // 前端显式传了 field 则优先,否则走期刊选定的推广领域 + // 领域来源优先级:前端传 field > 前端传 fetch_ids > journal_promotion_field + $fields = []; if ($field !== '') { - $query->where('ef.field', 'like', '%' . $field . '%'); - } elseif (!empty($promotionFields)) { - $query->where(function ($q) use ($promotionFields) { - foreach ($promotionFields as $idx => $fieldName) { - if ($idx === 0) { - $q->where('ef.field', 'like', '%' . $fieldName . '%'); - } else { - $q->whereOr('ef.field', 'like', '%' . $fieldName . '%'); - } - } - }); + $fields = [$field]; + } elseif ($fetchIds !== '') { + $fields = $this->resolveFieldsByFetchIds($fetchIds); + } else { + $fields = $this->resolveFieldsByJournal($journalId); } - if ($noRepeatDays > 0) { - $cutoff = time() - ($noRepeatDays * 86400); - $query->where(function ($q) use ($cutoff) { - $q->where('e.ltime', 0)->whereOr('e.ltime', '<', $cutoff); - }); - } - - $experts = $query - ->field('e.*') - ->group('e.expert_id') - ->limit(100) - ->select(); + $experts = $this->findEligibleExperts($fields, $noRepeatDays, 100, $targetPartitions, $targetCountryIds); } if (empty($experts)) { @@ -1488,26 +1452,31 @@ class EmailClient extends Base $now = time(); $taskId = Db::name('promotion_task')->insertGetId([ - 'journal_id' => $journalId, - 'template_id' => $templateId, - 'style_id' => $styleId, - 'scene' => $scene, - 'task_name' => $taskName ?: ('Task ' . date('Y-m-d H:i')), - 'total_count' => count($experts), - 'sent_count' => 0, - 'fail_count' => 0, - 'bounce_count' => 0, - 'state' => 0, - 'smtp_ids' => $smtpIds, - 'min_interval' => max(5, $minInterval), - 'max_interval' => max($minInterval, $maxInterval), - 'max_bounce_rate' => $maxBounceRate, - 'no_repeat_days' => $noRepeatDays, - 'send_start_hour' => $sendStartHour, - 'send_end_hour' => $sendEndHour, - 'send_date' => $sendDateVal, - 'ctime' => $now, - 'utime' => $now, + 'journal_id' => $journalId, + 'factory_id' => 0, + 'template_id' => $templateId, + 'style_id' => $styleId, + 'scene' => $scene, + 'type' => $type, + 'expert_type' => $expertType, + 'task_name' => $taskName ?: ('Task ' . date('Y-m-d H:i')), + 'total_count' => count($experts), + 'sent_count' => 0, + 'fail_count' => 0, + 'bounce_count' => 0, + 'state' => 0, + 'smtp_ids' => $smtpIds, + 'min_interval' => max(5, $minInterval), + 'max_interval' => max($minInterval, $maxInterval), + 'max_bounce_rate' => $maxBounceRate, + 'no_repeat_days' => $noRepeatDays, + 'send_start_hour' => $sendStartHour, + 'send_end_hour' => $sendEndHour, + 'send_date' => $sendDateVal, + 'target_partitions' => $targetPartitions, + 'target_country_ids' => $targetCountryIds, + 'ctime' => $now, + 'utime' => $now, ]); $logs = []; @@ -1931,6 +1900,33 @@ class EmailClient extends Base $item['progress'] = $item['total_count'] > 0 ? round(($item['sent_count'] + $item['fail_count']) / $item['total_count'] * 100, 1) : 0; + + $tp = isset($item['target_partitions']) ? trim((string)$item['target_partitions']) : ''; + $tc = isset($item['target_country_ids']) ? trim((string)$item['target_country_ids']) : ''; + if ($tp === '' && $tc === '') { + $item['country_scope_label'] = '全部国家'; + } else { + $parts = []; + if ($tp !== '') { + $parts[] = '分区' . $tp; + } + if ($tc !== '') { + $cids = array_map('intval', explode(',', $tc)); + $names = Db::name('country')->where('country_id', 'in', $cids)->column('zh_name'); + if (!empty($names)) { + $parts[] = implode(',', $names); + } + } + $item['country_scope_label'] = implode(' + ', $parts); + } + + $fid = isset($item['factory_id']) ? intval($item['factory_id']) : 0; + if ($fid > 0) { + $fRow = Db::name('promotion_factory')->where('promotion_factory_id', $fid)->field('promotion_factory_id, type, fetch_ids')->find(); + $item['factory_name'] = $fRow ? ('工厂#' . $fid) : '已删除'; + } else { + $item['factory_name'] = '手动创建'; + } } return jsonSuccess([ @@ -2124,35 +2120,35 @@ class EmailClient extends Base * 每日自动生成推广任务(由 Linux crontab 调用) * * 逻辑: - * 1. 查询所有 start_promotion=1 且 state=0 的期刊 - * 2. 对每个期刊,检查明天是否已存在任务,避免重复 - * 3. 用 default_template_id / default_style_id 创建 promotion_task - * 4. 根据期刊绑定的领域自动筛选专家(排除近7天已联系、排除非正常状态) - * 5. 每个期刊默认生成 100 封邮件 + * 1. 查询所有 state=0 的任务工厂(当前仅处理 expert_type=5) + * 2. JOIN journal 确认期刊有效(state=0, start_promotion=1) + * 3. 按 factory_id + send_date 检查去重 + * 4. template/style: 工厂 > 0 用工厂的,否则用期刊默认 + * 5. 用工厂的 fetch_ids 查领域,用工厂的 target_partitions/target_country_ids 做国家过滤 + * 6. 生成 promotion_task + promotion_email_log * * crontab 示例(每天凌晨1点执行): * 0 1 * * * curl -s "https://your-domain.com/api/email_client/cronDailyCreateTasks" >> /var/log/promotion_cron.log 2>&1 - * - * 配合已有定时任务: - * 22:00 prepareTasksForDate — 预渲染邮件内容 - * 08:00 triggerTasksForDate — 开始发送 */ public function cronDailyCreateTasks() { set_time_limit(120); - $sendDate = date('Y-m-d', strtotime('+1 day')); - $dailyLimit = 100; - $noRepeatDays = 7; + $sendDate = date('Y-m-d', strtotime('+1 day')); + $noRepeatDays = 30; - $journals = Db::name('journal') - ->where('start_promotion', 1) - ->where('state', 0) - ->where('default_template_id', '>', 0) + $factories = Db::name('promotion_factory') + ->alias('f') + ->join('t_journal j', 'j.journal_id = f.journal_id', 'inner') + ->where('f.state', 0) + ->where('f.expert_type', 5) + ->where('j.state', 0) + ->where('j.start_promotion', 1) + ->field('f.*, j.title as journal_title, j.default_template_id, j.default_style_id') ->select(); - if (empty($journals)) { - return jsonSuccess(['msg' => 'No journals with promotion enabled', 'created' => 0]); + if (empty($factories)) { + return jsonSuccess(['msg' => 'No active factories found', 'created' => 0]); } $created = 0; @@ -2160,13 +2156,12 @@ class EmailClient extends Base $errors = []; $details = []; - foreach ($journals as $journal) { - $journalId = $journal['journal_id']; - $templateId = intval($journal['default_template_id']); - $styleId = intval(isset($journal['default_style_id']) ? $journal['default_style_id'] : 0); + foreach ($factories as $factory) { + $factoryId = intval($factory['promotion_factory_id']); + $journalId = intval($factory['journal_id']); $existTask = Db::name('promotion_task') - ->where('journal_id', $journalId) + ->where('factory_id', $factoryId) ->where('send_date', $sendDate) ->where('state', 'in', [0, 1, 5]) ->find(); @@ -2175,59 +2170,90 @@ class EmailClient extends Base continue; } + $templateId = intval($factory['template_id']) > 0 + ? intval($factory['template_id']) + : intval($factory['default_template_id']); + $styleId = intval($factory['style_id']) > 0 + ? intval($factory['style_id']) + : intval(isset($factory['default_style_id']) ? $factory['default_style_id'] : 0); + + if ($templateId <= 0) { + $errors[] = 'factory_id=' . $factoryId . ': no template_id configured'; + continue; + } + $tpl = Db::name('mail_template') ->where('template_id', $templateId) ->where('journal_id', $journalId) ->where('state', 0) ->find(); if (!$tpl) { - $errors[] = 'journal_id=' . $journalId . ': template_id=' . $templateId . ' not found'; + $errors[] = 'factory_id=' . $factoryId . ': template_id=' . $templateId . ' not found'; continue; } - $smtpCount = Db::name('journal_email') - ->where('journal_id', $journalId) - ->where('state', 0) - ->count(); - if ($smtpCount == 0) { - $errors[] = 'journal_id=' . $journalId . ': no active SMTP account'; + $smtpIds = trim((string)$factory['email_ids']); + if ($smtpIds === '') { + $smtpCount = Db::name('journal_email') + ->where('journal_id', $journalId) + ->where('state', 0) + ->count(); + if ($smtpCount == 0) { + $errors[] = 'factory_id=' . $factoryId . ': no active SMTP account'; + continue; + } + } + + $fields = $this->resolveFieldsByFetchIds($factory['fetch_ids']); + if (empty($fields)) { + $errors[] = 'factory_id=' . $factoryId . ': no valid fields from fetch_ids'; continue; } - $experts = $this->findEligibleExperts($journal, $noRepeatDays, $dailyLimit); + $targetPartitions = trim((string)$factory['target_partitions']); + $targetCountryIds = trim((string)$factory['target_country_ids']); + $dailyLimit = max(1, intval($factory['send_count'])); + + $experts = $this->findEligibleExperts($fields, $noRepeatDays, $dailyLimit, $targetPartitions, $targetCountryIds); if (empty($experts)) { $details[] = [ + 'factory_id' => $factoryId, 'journal_id' => $journalId, - 'title' => $journal['title'], + 'title' => $factory['journal_title'], 'status' => 'no_experts', ]; continue; } - $now = time(); - $scene = $tpl['scene'] ?? 'promotion'; + $now = time(); + $scene = $tpl['scene'] ?? 'promotion'; $taskId = Db::name('promotion_task')->insertGetId([ - 'journal_id' => $journalId, - 'template_id' => $templateId, - 'style_id' => $styleId, - 'scene' => $scene, - 'task_name' => 'Auto-' . ($journal['title'] ?? $journalId) . '-' . $sendDate, - 'total_count' => count($experts), - 'sent_count' => 0, - 'fail_count' => 0, - 'bounce_count' => 0, - 'state' => 0, - 'smtp_ids' => '', - 'min_interval' => 30, - 'max_interval' => 60, - 'max_bounce_rate' => 5, - 'no_repeat_days' => $noRepeatDays, - 'send_start_hour' => 8, - 'send_end_hour' => 22, - 'send_date' => $sendDate, - 'ctime' => $now, - 'utime' => $now, + 'journal_id' => $journalId, + 'factory_id' => $factoryId, + 'template_id' => $templateId, + 'style_id' => $styleId, + 'scene' => $scene, + 'type' => intval($factory['type']), + 'expert_type' => intval($factory['expert_type']), + 'task_name' => 'Auto-' . ($factory['journal_title'] ?? $journalId) . '-F' . $factoryId . '-' . $sendDate, + 'total_count' => count($experts), + 'sent_count' => 0, + 'fail_count' => 0, + 'bounce_count' => 0, + 'state' => 0, + 'smtp_ids' => $smtpIds, + 'min_interval' => 30, + 'max_interval' => 60, + 'max_bounce_rate' => 5, + 'no_repeat_days' => $noRepeatDays, + 'send_start_hour' => 8, + 'send_end_hour' => 22, + 'send_date' => $sendDate, + 'target_partitions' => $targetPartitions, + 'target_country_ids' => $targetCountryIds, + 'ctime' => $now, + 'utime' => $now, ]); $logs = []; @@ -2248,8 +2274,9 @@ class EmailClient extends Base $created++; $details[] = [ + 'factory_id' => $factoryId, 'journal_id' => $journalId, - 'title' => $journal['title'], + 'title' => $factory['journal_title'], 'task_id' => $taskId, 'expert_count' => count($experts), 'send_date' => $sendDate, @@ -2325,12 +2352,31 @@ class EmailClient extends Base } /** - * 根据期刊选定的推广领域(journal_promotion_field → expert_fetch)筛选合适的专家 + * 根据 fetch_ids(逗号分隔的 expert_fetch_id)查出 field 名称数组 */ - private function findEligibleExperts($journal, $noRepeatDays, $limit) + private function resolveFieldsByFetchIds($fetchIds) { - $journalId = intval($journal['journal_id']); + $fetchIds = trim((string)$fetchIds); + if ($fetchIds === '') { + return []; + } + $ids = array_map('intval', explode(',', $fetchIds)); + $ids = array_filter($ids); + if (empty($ids)) { + return []; + } + $fields = Db::name('expert_fetch') + ->where('expert_fetch_id', 'in', $ids) + ->where('state', 0) + ->column('field'); + return array_values(array_unique(array_filter(array_map('trim', $fields)))); + } + /** + * 根据 journal_promotion_field 查出期刊绑定的领域名称数组(旧逻辑兼容) + */ + private function resolveFieldsByJournal($journalId) + { $fields = Db::name('journal_promotion_field') ->alias('jpf') ->join('t_expert_fetch ef_fetch', 'ef_fetch.expert_fetch_id = jpf.expert_fetch_id', 'inner') @@ -2338,9 +2384,49 @@ class EmailClient extends Base ->where('jpf.state', 0) ->where('ef_fetch.state', 0) ->column('ef_fetch.field'); + return array_values(array_unique(array_filter(array_map('trim', $fields)))); + } + /** + * 根据 target_partitions + target_country_ids 解析出最终要筛选的 country_id 列表 + * 两者取并集;都为空则返回空数组,表示不做国家过滤 + */ + private function resolveCountryIds($targetPartitions, $targetCountryIds) + { + $ids = []; - $fields = array_unique(array_filter(array_map('trim', $fields))); + if ($targetPartitions !== '') { + $parts = array_map('intval', explode(',', $targetPartitions)); + $parts = array_filter($parts); + if (!empty($parts)) { + $partIds = Db::name('country') + ->where('partition', 'in', $parts) + ->column('country_id'); + $ids = array_merge($ids, $partIds); + } + } + + if ($targetCountryIds !== '') { + $extra = array_map('intval', explode(',', $targetCountryIds)); + $ids = array_merge($ids, $extra); + } + + return array_values(array_unique(array_filter($ids))); + } + + /** + * 根据领域列表 + 国家筛选 + noRepeatDays 筛选合适的专家 + * + * @param array $fields 领域名称数组(由调用方从 factory.fetch_ids 或 journal_promotion_field 查好传入) + * @param int $noRepeatDays 同一专家N天内不重复 + * @param int $limit 最大返回数 + * @param string $targetPartitions 国家分区,逗号分隔 + * @param string $targetCountryIds 单独指定的 country_id,逗号分隔 + * @return array + */ + private function findEligibleExperts($fields, $noRepeatDays, $limit, $targetPartitions = '', $targetCountryIds = '') + { + $fields = array_values(array_unique(array_filter(array_map('trim', $fields)))); if (empty($fields)) { return []; @@ -2368,6 +2454,11 @@ class EmailClient extends Base }); } + $countryIds = $this->resolveCountryIds($targetPartitions, $targetCountryIds); + if (!empty($countryIds)) { + $query->where('e.country_id', 'in', $countryIds); + } + return $query ->field('e.*') ->group('e.expert_id') diff --git a/application/api/controller/PromotionFactory.php b/application/api/controller/PromotionFactory.php new file mode 100644 index 0000000..2c39b8e --- /dev/null +++ b/application/api/controller/PromotionFactory.php @@ -0,0 +1,263 @@ +request->param('journal_id', 0)); + $editor_id = $this->request->param("user_id",0); + $state = $this->request->param('state', '-1'); + $page = max(1, intval($this->request->param('page', 1))); + $perPage = max(1, min(intval($this->request->param('per_page', 20)), 100)); + +// if (!$journalId) { +// return jsonError('journal_id is required'); +// } + if($journalId!==0){ + $where[] = ['journal_id', '=', $journalId]; + }else{ + $journalIds = $this->journal_obj->where("editor_id",$editor_id)->where("state",0)->column("journal_id"); + $where[] = ['journal_id',"in",$journalIds]; + } + + if ($state !== '-1' && $state !== '') { + $where[] = ['state', '=', intval($state)]; + } + + $countQuery = Db::name('promotion_factory'); + foreach ($where as $w) { + $countQuery->where($w[0], $w[1], $w[2]); + } + $total = $countQuery->count(); + + $listQuery = Db::name('promotion_factory'); + foreach ($where as $w) { + $listQuery->where($w[0], $w[1], $w[2]); + } + $list = $listQuery + ->order('promotion_factory_id desc') + ->page($page, $perPage) + ->select(); + + foreach ($list as &$item) { + $item['ctime_text'] = $item['ctime'] ? date('Y-m-d H:i:s', $item['ctime']) : ''; + $item['fetch_fields'] = $this->resolveFetchFields($item['fetch_ids']); + $item['country_scope_label'] = $this->buildCountryScopeLabel( + isset($item['target_partitions']) ? (string)$item['target_partitions'] : '', + isset($item['target_country_ids']) ? (string)$item['target_country_ids'] : '' + ); + } + + return jsonSuccess([ + 'list' => $list, + 'total' => $total, + 'page' => $page, + 'per_page' => $perPage, + 'total_pages' => $total > 0 ? ceil($total / $perPage) : 0, + ]); + } + + /** + * 工厂详情 + * 参数: promotion_factory_id + */ + public function getDetail() + { + $id = intval($this->request->param('promotion_factory_id', 0)); + if (!$id) { + return jsonError('promotion_factory_id is required'); + } + + $row = Db::name('promotion_factory')->where('promotion_factory_id', $id)->find(); + if (!$row) { + return jsonError('Factory not found'); + } + + $row['ctime_text'] = $row['ctime'] ? date('Y-m-d H:i:s', $row['ctime']) : ''; + $row['fetch_fields'] = $this->resolveFetchFields($row['fetch_ids']); + $row['country_scope_label'] = $this->buildCountryScopeLabel( + isset($row['target_partitions']) ? (string)$row['target_partitions'] : '', + isset($row['target_country_ids']) ? (string)$row['target_country_ids'] : '' + ); + + return jsonSuccess($row); + } + + /** + * 新增工厂 + */ + public function add() + { + $data = $this->request->post(); + + $journalId = intval(isset($data['journal_id']) ? $data['journal_id'] : 0); + if (!$journalId) { + return jsonError('journal_id is required'); + } + + $journal = Db::name('journal')->where('journal_id', $journalId)->where('state', 0)->find(); + if (!$journal) { + return jsonError('Journal not found'); + } + + $now = time(); + $id = Db::name('promotion_factory')->insertGetId([ + 'journal_id' => $journalId, + 'type' => intval(isset($data['type']) ? $data['type'] : 0), + 'expert_type' => intval(isset($data['expert_type']) ? $data['expert_type'] : 5), + 'email_ids' => trim(isset($data['email_ids']) ? (string)$data['email_ids'] : ''), + 'send_count' => max(1, intval(isset($data['send_count']) ? $data['send_count'] : 100)), + 'template_id' => intval(isset($data['template_id']) ? $data['template_id'] : 0), + 'style_id' => intval(isset($data['style_id']) ? $data['style_id'] : 0), + 'fetch_ids' => trim(isset($data['fetch_ids']) ? (string)$data['fetch_ids'] : ''), + 'target_partitions' => trim(isset($data['target_partitions']) ? (string)$data['target_partitions'] : ''), + 'target_country_ids' => trim(isset($data['target_country_ids']) ? (string)$data['target_country_ids'] : ''), + 'state' => 0, + 'ctime' => $now, + ]); + + return jsonSuccess(['promotion_factory_id' => $id]); + } + + /** + * 编辑工厂 + */ + public function edit() + { + $data = $this->request->post(); + $id = intval(isset($data['promotion_factory_id']) ? $data['promotion_factory_id'] : 0); + if (!$id) { + return jsonError('promotion_factory_id is required'); + } + + $row = Db::name('promotion_factory')->where('promotion_factory_id', $id)->find(); + if (!$row) { + return jsonError('Factory not found'); + } + + $update = []; + $allowedFields = [ + 'type', 'expert_type', 'email_ids', 'send_count', + 'template_id', 'style_id', 'fetch_ids', + 'target_partitions', 'target_country_ids', + ]; + $intFields = ['type', 'expert_type', 'send_count', 'template_id', 'style_id']; + + foreach ($allowedFields as $f) { + if (isset($data[$f])) { + $update[$f] = in_array($f, $intFields) ? intval($data[$f]) : trim((string)$data[$f]); + } + } + + if (isset($update['send_count'])) { + $update['send_count'] = max(1, $update['send_count']); + } + + if (empty($update)) { + return jsonError('没有可更新的字段'); + } + + Db::name('promotion_factory')->where('promotion_factory_id', $id)->update($update); + + return jsonSuccess([]); + } + + /** + * 删除工厂(软删除 state=1) + */ + public function delete() + { + $id = intval($this->request->param('promotion_factory_id', 0)); + if (!$id) { + return jsonError('promotion_factory_id is required'); + } + + $row = Db::name('promotion_factory')->where('promotion_factory_id', $id)->find(); + if (!$row) { + return jsonError('Factory not found'); + } + + Db::name('promotion_factory')->where('promotion_factory_id', $id)->update(['state' => 1]); + + return jsonSuccess([]); + } + + /** + * 启用/禁用切换 + */ + public function toggle() + { + $id = intval($this->request->param('promotion_factory_id', 0)); + if (!$id) { + return jsonError('promotion_factory_id is required'); + } + + $row = Db::name('promotion_factory')->where('promotion_factory_id', $id)->find(); + if (!$row) { + return jsonError('Factory not found'); + } + + $newState = ($row['state'] == 0) ? 1 : 0; + Db::name('promotion_factory')->where('promotion_factory_id', $id)->update(['state' => $newState]); + + return jsonSuccess(['state' => $newState]); + } + + /** + * 根据 fetch_ids 解析出领域名称列表 + */ + private function resolveFetchFields($fetchIds) + { + $fetchIds = trim((string)$fetchIds); + if ($fetchIds === '') { + return []; + } + $ids = array_map('intval', explode(',', $fetchIds)); + $ids = array_filter($ids); + if (empty($ids)) { + return []; + } + return Db::name('expert_fetch') + ->where('expert_fetch_id', 'in', $ids) + ->column('field', 'expert_fetch_id'); + } + + /** + * 构建国家范围描述标签 + */ + private function buildCountryScopeLabel($targetPartitions, $targetCountryIds) + { + $tp = trim($targetPartitions); + $tc = trim($targetCountryIds); + + if ($tp === '' && $tc === '') { + return '全部国家'; + } + + $parts = []; + if ($tp !== '') { + $parts[] = '分区' . $tp; + } + if ($tc !== '') { + $cids = array_map('intval', explode(',', $tc)); + $names = Db::name('country')->where('country_id', 'in', $cids)->column('zh_name'); + if (!empty($names)) { + $parts[] = implode(',', $names); + } + } + return implode(' + ', $parts); + } +} diff --git a/sql/add_country_scope_to_promotion_task.sql b/sql/add_country_scope_to_promotion_task.sql new file mode 100644 index 0000000..aaf4305 --- /dev/null +++ b/sql/add_country_scope_to_promotion_task.sql @@ -0,0 +1,3 @@ +ALTER TABLE t_promotion_task + ADD COLUMN target_partitions VARCHAR(32) NOT NULL DEFAULT '' COMMENT '目标分区,逗号分隔,如 1,2,3;空=不限', + ADD COLUMN target_country_ids VARCHAR(255) NOT NULL DEFAULT '' COMMENT '单独指定的国家ID,逗号分隔;空=不额外指定';